原型链大都是围绕着对象来进行的,在此之前,我们需要知道一个问题:
创建对象的方法有几种,区别是什么?
创建对象的常用的几种方法
1.字面量
let obj1 = {
name: 'laocao',
age: 22,
sayHi: () => {
console.log(`Hello,this is${name}`);
}
};
缺点:复用性差,也就是说,当创建相同类型对象的实例的时候,需要重复写代码
2.工厂模式创建对象
function createObject(name , age) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.sayHi = () => {
console.log(`Hello,this is${name}`);
};
return obj;
}
let obj2 = createObject('laocao', 22);
解决了字面量方式复用性差的缺点,把创建对象的过程封装在一个函数,最后返回这个对象的实例,以后创建大量相同类型的实例只需调用函数即可。
缺点:无法识别对象的类型,大家都是通过new Object创建的,我怎么知道某个实例来自于哪个对象的呢?
从本质上来说,上面两种创建对象的方式原理是一样的,因为原型链是一致的,在浏览器控制台测试:
为了解决对象识别问题,又有一种方式创建对象了:自定义构造函数
3.自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHi = () => {
console.log(`Hello,this is${name}`);
};
}
let obj3 = new Person('laocao',22);
相对于上面一种方法,这种方法有效解决了对象类型的问题,而且为了区别与普通函数,构造函数首字母要大写,除此之外,需要通过new关键字来调用这个函数,将this指向当前实例对象,否则,这个函数是window对象的。总结自定义构造函数创建对象:
做了四件事:
1.在堆内存中开辟(申请一块空闲的,new)空间,存储新的对象
2.把this设置成当前对象
3.设置属性值和方法值
4.返回这个对象(实例或者是说对象的引用,在栈存储)
ps:图画的可能不太标准,内存名是我随便找的,大概是这种格式。
好了新问题来了,我们在创建一个对象的实例
let obj4 = new Person('laocao',22);
然后在浏览器判断obj3与obj4的方式是不是一样的
很显然,这是两个不同的方法,因为在堆中申请了两块内存(new了两次),所以如果想使用这个方法,需要再次创建对象,这样就浪费了内存。
4.Object.create(proto, [propertiesObject])方式
proto:新创建对象的原型对象
返回值:在指定原型对象上添加新属性后的对象
后面参数具体用法可以查看MDN
MDN原话:
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
其实也就是实现继承,具体继承哪个对象,就看里面参数指向哪个对象的原型对象了
通过这种方式创建的对象,它的属性和方法都来自它所继承的原型对象中
比如:创建空对象,也就是Object的原型对象
因为还没有将原型、原型链、实例对象、构造函数、原型对象之间的关系,所以先讲述它们之间的关系。
原型、原型链、实例对象、构造函数、原型对象
我把自定义构造函数创建对象那个例子改一下:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = () => {
console.log(`Hello,this is${this.name}`);
};
let obj3 = new Person('laocao',22);
let obj4 = new Person('laocai',20);
此时我在浏览器测试:
sayHi那个方法不见了,而且两个实例化对象的方法是一样的了,这是为啥呢?
在回答这个问题之前,先用一张图来看看原型,原型对象,构造函数,实例对象的关系:
①定义构造函数Person,添加属性和方法
在定义函数Person的时候,浏览器会给这个函数加上prototype属性,这个属性就是原型,也是一个对象,在控制台输出Person.prototype就可知,prototype不仅是Person的属性,也是它的原型对象
那么在这个原型对象里面,有一个sayHi方法,有constructor属性和__proto__属性。而constructor这个属性指向的就是Person这个函数,说明Person原型对象prototype的构造器是Person。这个原型对象里面的__proto__指向的是Object的原型对象prototype
这个函数里面其实还有一个__proto__属性,它指向的是Funtion的原型对象,而Function的原型对象里面的__proto__指向的就是顶级对象Object的原型对象
②实例化对象
通过new运算符就实例化了一个对象 ,这个实例化对象里面有一个__proto__属性,这个属性也叫原型,同时也是原型对象,它指向的是构造函数Person的原型对象prototype
总结:
-
对象一定有__proto__,函数一定有prototype,函数里面其实可能还有__proto__,因为函数也是对象,它是由Function创建的
-
通过实例对象的__proto__和构造函数的原型对象prototype之间的联系就形成了原型链,原型链的顶端是null,它是Object的原型对象
-
__proto__是浏览器里面的,现在还不规范,而prototype现在成为了标准
再回到上面的问题,sayHi方法放在了在Person的原型对象中,所以这个方法是可以被共享的,这样节省了空间。
上面所有过程的测试:
instanceof运算符的原理
判断实例对象是哪种类型
背后的原理其实就是判断实例对象的__proto__以及它的原型链和构造函数的prototype是不是指向同一块地址,因为Object和Person在obj3的原型链上,所以系统判断为是同一个引用地址,返回true,如果需要严格判断可以让实例对象中的__proto__所指向原型对象的构造器和构造函数进行比较
new运算符的原理
还是那上面的obj3来举例
1.创建一个继承于Person.prototype的新对象
2.执行构造函数Person,传进相应的参数,将this指向这个新对象的实例
3.如果构造函数返回了一个对象,那么就代替new 创建的对象
//模拟new运算符的原理,参数fn是构造函数
let newObject = fn=> {
//1.创建一个继承于构造函数的原型对象的新对象obj
let obj = Object.create(fn.prototype);
//2.执行构造函数,并将this指向当前实例对象
let o = fn.call(obj);
//3.判断构造函数有没有返回值o
if (typeof o === 'object') {
return o;
} else {
return obj;
}
};