说原型链的时候我们先说个更基础的知识,
数据类型,JS大致分为三种类型,Number、基本类型、引用类型。
Number Boolean undefined Object Function String Null
基本类型:Number Boolean String undefined null
引用类型:Object Function
- 基本类型的数据是存放在栈内存中的,而引用类型的数据是存放在堆内存中的
什么是堆(heap)和栈(stack)
- 栈(stack) 会自动分配内存空间,会自动释放。
var a = 1;
var b = a;
var a = 3;
console.log(a,b) // 3 1
- 也就是说,基本类型的复制就是在栈内存中开辟出了一个新的存储区域用来存储新的变量,这个变量有它自己的值,只不过和前面的值一样,所以如果其中一个的值改变,则不会影响到另一个。
- 堆(heap) 动态分配的内存,大小不定也不会自动释放。
var objA = new Object();
var objB = objA;
objA.name = 'obja';
console.log(objA);
console.log(objB);
{name: "obja"}
{name: "obja"}
- 你会发现你修改了objA的时候,objB也同时被修改了。是为什么呢?
其实引用类型定义一个对象是在栈内存中存储了一个指针,这个指针指向堆内存中该对象的存储地址。复制给另一个对象的过程并不是开建一个新的内存空间,而是把该对象的地址复制给了另一个对象变量,两个指针都指向同一个对象,所以若其中一个修改了,则另一个也会改变。
补充一个小知识:
- 基本数据本来是没有方法的,引用数据类型才有,但是字符串是有很多方法的,比如indexOf(),这是为什么呢?
var str = "123456";
var strIndex = str.indexOf("1");
console.log(strIndex); // 0
其实js底层干了这么一件事:
var str = new String("123456");
var strIndex = str.indexOf("1");
string = null;
- 1、创建一个String类型的实例;2、在实例上调用指定方法;3、销毁该实例
- 使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。
搞清楚了引用类型,搞清楚了堆内存,搞清楚了指针,我们再继续搞原型链。
首先搞清楚什么是原型链。
function Person(name) {
this.name = name
}
var test = new Person('test');
var str = test.toString()
console.log(test)
console.log(str)
// 打印结果
Person{name: "test"}
[object Object]
问题来了,我们的Person有定义toString()方法吗?为什么没报错呢?还打印出来结果了?
我们把打印结果展开看看
image.png
原来函数居然自带了一个____proto____,里面居然有这么多方法,还有constructor和prototype。这都是些啥?
这就是原型链,而原型链最难理解的就是这几个属性:prototype,constructor,____proto____,只要理解了它们,你就能理解原型链。理解他们我们得先理解函数。
什么是构造函数
- 在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写。看清楚这个调用,他不是new一个函数,而是用new来调用。
function Person(name) {
this.name = name
}
var test = new Person('test');
这就是一个构造函数。
构造函数有什么意义?
其实就是JS一个面向对象的设计
看代码:
function Book(name,author) {
this.name = name;
this.author = author;
this.detail = function() {
console.log("书名:" + this.name + ", 作者:" + this.author);
};
}
var book1 = new Book("二手时间", "斯韦特兰娜·亚历山德罗夫娜·阿列克谢耶维奇");
var book2 = new Book("巨人的陨落", "肯·福莱特");
console.log(book1 instanceof Book); // true
console.log(book2 instanceof Book); // true
console.log(book1.constructor === Book); // true
console.log(book2.constructor === Book); // true
book1.detail();
book1.detail();
// 书名:二手时间, 作者:斯韦特兰娜·亚历山德罗夫娜·阿列克谢耶维奇
// 书名:巨人的陨落, 作者:肯·福莱特
**
- instanceof 运算符来推断对象的类型证明book1和book2是Book类型的实例。
- constructor 构造函数属性指向该构造函数。
- detail()方法打印的内容说明:我们可以使用 Book 构造函数创建对象带有初始化的名称属性
这就是构造函数,构造函数对于原型链的理解不是最主要的,但是你也很重要的一环,我怕有人不懂构造函数,所以在此简单粗略的讲了一下。我们继续讲原型链。
函数的本质其实是对象。
- 你也看到我的举例就是一个函数,在JS中,函数的本质就是对象,它与其他对象不同的是,创建它的构造函数与创建其他对象的构造函数不一样。那产生函数对象的构造函数是什么呢?是一个叫做Function的特殊函数,通过new Function产生的对象就是一个函数。
new Function
function f1() {}
//上面的函数等同于:
var f1 = new Function();
function sum(a, b) {
return a + b;
}
//上面的函数等同于:
var sum = new Function("a", "b", "return a + b"); // 前面的是函数形参名,最后一个参数是函数体
- 也就是说:只要通过Function创建的对象就是函数,函数都是通过Function创建的。
看图
image.png
以上我们可以看到普通对象是由函数创建的,函数是由Function创建的。那我们会有一个疑问Function是从哪里来的?其实Function是不通过其他函数得到,它是JS执行引擎初始化就直接通过本地代码直接放置到内存中的。
当一个函数被创建后,这个函数就会自动附带一个属性prototype,它就是一个Object对象,代表着函数的原型。也就是说prototype就是原型对象
原型对象中包含两个属性:constructor和____proto____。constructor这个属性是指创建原型的函数,它指向函数本身。所以有以下关系:
image.png
上代码
var Person = function () { };
var p = new Person();
这个new做了什么呢?
我们把 new 的过程拆分成以下三步:
1、var p={}; 也就是说,初始化一个对象p。
2、p.____proto____=Person.prototype;
3.、Person.call(p);也就是说构造p,也可以称之为初始化p。
我们来证明一下:____proto____=Person.prototype
var Person = function () { };
var p = new Person();
console.log(p.__proto__ === Person.prototype); // true
这段代码会返回 true。说明我们步骤2是正确的。
那么____proto____是什么?
每个对象都会在其内部初始化一个属性,就是 ____proto____,当我们访问一个对象的属性 时,如果这个对象内部不存在这个属性,那么他就会去____proto____里找这个属性,这个____proto____又会有自己的____proto____,于是就这样 一直找下去,也就是我们平时所说的原型链的概念。
终于扯回原型链了!!!
按照标准,____proto____是不对外公开的,也就是说是个私有属性,但是 Firefox 的引擎将他暴露了出来成为了一个共有的属性,我们可以对外访问和设置。
- 上代码:
var Person = function () { };
Person.prototype.Say = function () {
console.log("Person say");
}
var p = new Person();
p.Say(); // Person say
很多人看到这儿还是一脸懵逼的对不对?我抽象一点儿吧!顺藤摸瓜,____proto____就是藤,还记得上面的____proto____=Person.prototype等于true吧,其实prototype也是藤,只不过有时候这个藤分了好几个叉,不见得第一个就能摸到瓜,需要一直往上摸,摸到最顶端就是NULL。
我们在形象点,proto单词意思叫原型,那么____proto____这加几个下划线像啥,像链子呗,所以原型链说到底就是____proto____,prototype只是一个假象,它其实只在new的时候起作用。
再看一段代码:
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
这很关键,有人把中间的瓜摘了,但是只要藤上还有瓜,我们就能继续顺着藤子往上摸。可如果没瓜呢?就会报错,undefined。因为我们的藤子不能无限长,所以呢我们的藤兜就是Object。
console.log(Object.prototype.__proto__ === null) // true
Object就没藤了,没____proto____了,所以js作者用了一个null来终结。
为啥挺简单的事儿整这么复杂呢?
原型链的设计是因为堆内存的访问,堆内存是为了省内存。原型链就这么诞生了!原型链你简单点儿想,他是一个指向内存的方向牌,因为对象函数开发时往往是很大的数据类型,如果采用栈内存的方式,会严重拖累运行速度。才有了堆内存和原型链。
搞懂了原型链还得搞懂如何深度克隆,搞懂This指向问题。当你能搞懂原型链还能搞懂this的时候还能理解深度克隆,那你就算理解javascript了。
this指向问题
先记住两点
- this永远指向一个对象;
- this的指向完全取决于函数调用的位置;
- 在特殊函数里指向window,例如自执行函数、setInterval、setTimeout
上代码
console.log(this);// window{}
你看js多牛逼,哪怕啥也没有,this就能打印一个window对象。你以为这是好事?
上代码
window.number = 5;
var obj = {
'number': 10,
'sayThis': (function(){
this.number *= 10
console.log(this.number)
return function() {
this.number *= 2
console.log(this.number)
}
})()
}
obj.sayThis()
你猜猜会打印啥?我告诉你会打印50,20。这是人能理解的this??
自己满满消化吧,这一题看懂你也就懂了。
怎么手动改变this指向(call()、apply()、bind() )
- 哎,咱搞不清楚他指向哪儿,但咱可以手动改变this指向。
这有啥好处呢?好处就是打个比方,你突然想造个花坛,可是你没锄头,邻居刚好就有,你这个时候是买一把还是借用一下科学呢?还原到程序世界就是,我新建一个对象需要一个方法是其他对象有的,我们就可以通过改变this借用。于是有了这几个方法。
call,apply,bind的基本介绍和使用方式。
语法:
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值:
- call/apply:fun执行的结果
- bind:返回fun的拷贝,并拥有指定的this值和初始参数
上代码:
var obj = {
name: '小明',
sayHello: function(father, mother) {
console.log(this.name + "说 hello world")
},
details: function (city, father) {
console.log(`我叫${this.name},来自${city},爹是${father}`)
}
};
var person = { name : '张三'};
obj.sayHello(); // 小明说 hello world
obj.sayHello.call(person); // 张三说 hello world
obj.sayHello.apply(person); // 张三说 hello world
obj.sayHello.bind(person)(); // 张三说 hello world
obj.details('无聊的程序员', 'Z_D'); // 我叫小明,来自无聊的程序员,爹是Z_D
obj.details.call(person, '罗翔说刑法', '罗翔'); // 我叫张三,来自罗翔说刑法,爹是罗翔
obj.details.apply(person, ['罗翔说刑法', '罗翔']); // 我叫张三,来自罗翔说刑法,爹是罗翔
obj.details.bind(person, '罗翔说刑法', '罗翔')(); // 我叫张三,来自罗翔说刑法,爹是罗翔
看懂这一个例子基本上也就ojbk了
注意事项
- 调用call/apply/bind的必须是个函数
实际使用开拓
- 我们不能只用它来借用我们生成的一些构造函数啊对象,原生的一样可以借用。
获取数组的最大最小值
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
类数组借用数组的push方法
var arrayLike = {
0: 'OB',
1: 'Koro1',
length: 2
}
Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
搞定了this,大致搞定了哈,还可以深入学习,比如手写call apply的实现,大厂常见面试题。自行研究,我们来继续搞深度克隆。
- 为什么需要克隆呢?因为我们有时候业务需求需要一个同样的对象,但是因为对象是引用类型,你修改会影响原型的值,所以我们才有了克隆。
浅克隆,针对子项没有引用类型的对象
var jack = {
age: 25,
gender: "男"
};
function clone(obj){
var newObj = {};
for(var key in obj){
newObj[key] = obj[key];
}
return newObj;
}
var jackCopy = clone(jack);
console.log(jackCopy);
console.log(jackCopy==jack);
深克隆,对象下面还有引用类型使用
{ details : { name : '123' } }
克隆后修改还是会影响原型对象的。
var jack = {
age:25,
gender:"男",
address:{
province:"河南",
city:"周口"
},
friend:["tom","rose","bob"]
};
function deepClone(obj){
//如果对象是null
if(obj===null){
return null;
}else if({}.toString.call(obj)==="[object Array]"){
//如果对象是数组
var newArr = [];
newArr = obj.slice();//全部截取obj数组到newArr中
return newArr;
}
var newObj = {};
for(var key in obj){
// 如果原对象的当前属性是原始值
if(typeof obj[key]!=="object"){
newObj[key] = obj[key]
}else{
// 如果当前属性还是引用类型就再次调用deepClone()函数
newObj[key] = deepClone(obj[key])
}
}
return newObj;
}
var jackCopy = deepClone(jack);
jackCopy.address.province = "上海";
console.log(jack);
console.log(jackCopy);
-
这就实现了深克隆
-
是最简单粗暴的深拷贝:
var newObject = JSON.parse(JSON.stringify(oldObject));
-
能够处理JSON格式的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝,而且会直接丢失相应的值,还有就是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理:
网友评论