
目录
进入原型讲解之前,我们需要知道两个概念
1、首先,构造函数是一个对象(函数就是在特定上下文中执行代码的简单对象)!
2、其次,原型对象本身也是对象!
原型和原型对象的区别:
我们有时候会见到这么一段描述:每个对象都有原型,对象的原型指向原型对象
其实,他们之间的关系就是key:value的关系,原型是key,value是原型对象
所以,我们才会说原型指向原型对象,我们不必过分纠结两者的关系,当我们看见什么指向什么时,大都可以理解为key:value的关系
一、自定义构造函数
每个函数都会创建一个 prototype 属性,这个属性的值就是原型对象,所以可通过.prototype获取原型对象
那么这个原型对象从何而来,我们接着往下看...
function Person() {}
// 获取原型对象
console.log(Person.prototype) // Person的原型对象
console.log(Object.prototype) // Object的原型对象
1. 原型对象默认会获得constructor属性,指回与之关联的构造函数(constructor: ƒ Person()),
其他方法则继承自Object(至于为什么继承自Object,我们先暂且搁置,后面自会豁然开朗)
- constructor是一个不可枚举属性,可通过以下方法查看
let observe = Object.getOwnPropertyDescriptors(Person.prototype) console.log(observe)
- 所以你会发现constructor是一个灰色的属性
2. 你会在原型对象下看到[[Prototype]]: Object,这个就是Object的原型对象
3. 总的来说,可以理解为 [[Prototype]] 就是构造函数的原型对象,可通过构造函数的prototype属性获取(即prototype指向原型对象[[Prototype]])
图一
二、 调用构造函数
调用构造函数,即new之后,js都做了哪些事?
具体过程可阅读工厂模式与构造函数模式的区别最后部分
let person1 = new Person()
// 给原型对象添加其他属性和方法
Person.prototype.name = 'jack'
Person.prototype.sayName = () => {
console.log(this.name)
}
console.log(person1)
图二
刚开始我们就说到,原型对象也是对象,即然如此,那原型对象也应该有原型对象
如果这么理解,那岂不是儿子的爸爸的爸爸的爸爸的······是谁?
为了解决这个疑问,我们来看看爸爸是谁:
1. 正常的原型链都会终止于 Object 的原型对象,Object 原型对象的原型是null(Object.prototype.__proto__)
2. Firefox、Safari 和 Chrome会在每个原型对象上暴露__proto__属性,通过这个属性可以 访问原型对象的原型对象(因为原型对象本身也是对象)
console.log(Object.prototype)
图三
到这里我们就知道了,原来爸爸是我 😁
三、构造函数、原型对象和实例之间的关系
1. 构造函数、原型对象和实例是 3 个完全不同的对象
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
2. 实例通过__proto__链接到原型对象,即指向隐藏特性 [[Prototype]]
如果不理解,可以对比构造函数:
构造函数是通过 prototype 属性获取的原型对象[[Prototype]],prototype:[[Prototype]]
实例则是通过 __proto__ 属性获取的原型对象[[Prototype]], __proto__:[[Prototype]]
实例.__proto__ 就相当于 构造函数.prototype
同一个构造函数创建的两个实例共享同一个原型对象
let person2 = new Person()
console.log(person1.__proto__ === person2.__proto__); // true
// constructor 可以用于标识对象类型
console.log(person1 instanceof Person); // true
// person1 之所以也被认为是Object的实例,是因为所有自定义对象都继承自Object
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
3. 构造函数通过 prototype 属性指向 原型对象
4. 结论:实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true
console.log(Person.prototype.constructor === Person); // true
ps:如果不理解这个图,建议对比我们之前打印的结果(图一和图二),或许对你有更好的帮助
关系图
四、isPrototypeOf()
1. 不是所有实现都对外暴露了[[Prototype]],但可以使用 isPrototypeOf()方法确定两个对象之间关系
2. 通过原型对象调用 isPrototypeOf()方法检查了person1,这个实例内部的constructor指向Person.prototype(即Person的原型对象),所以返回true
console.log(Person.prototype.isPrototypeOf(person1)); // true
ps:不理解请看上面的关系图
五、Object.getPrototypeOf()
返回参数的内部特性[[Prototype]]的值,即取得一个对象的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1));
六、Object.setPrototypeOf()
1. 向实例的私有特性 [[Prototype]] 写入一个新值,这样就可以重写一个对象的原型继承关系
2. 由于同一个构造函数创建的多个实例共享同一个原型对象,所以可能会严重影响代码性能, 会涉及所有访问了[[Prototype]]的对象的代码
let biped = {
numLegs: 2
};
let person = {
name: 'jack'
};
Object.setPrototypeOf(person, biped);
console.log(person)
console.log(person.name); // jack
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
七、Object.create()
为避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创建一个新对象,同时为其指定原型
let biped1 = {
numLegs: 2
};
let persons = Object.create(biped1);
persons.name = 'jack';
console.log(persons)
console.log(persons.name); // jack
console.log(persons.numLegs); // 2
console.log(Object.getPrototypeOf(persons) === biped1); // true
八、原型链
原型链并没有我们想象的那么复杂,他就是一个找爸爸的过程
在前面我们就提到,每个对象都有原型对象,原型对象又有原型对象,......
当然,我们不可能永无休止的找爸爸,当我们找到null时,也就是Object 原型对象的原型时
我们就停下来了
爸爸 => 爸爸 => 爸爸 => ······ => null
这种原型层层连接起来就构成了我们所说的 原型链
九、总结
每个函数都会创建一个 prototype 属性,这个属性是一个对象
现在,我们回到第一部分抛出的两个问题:
这个对象从何而来?为什么说所有自定义对象都继承自Object?
通过了解调用构造函数(new)执行的操作后(如果不清楚,可参考 工厂模式与构造函数模式的区别最后部分),我们可以知道:
实际上,这个对象就是调用构造函数(new)时创建的 对象的 原型,
即prototype:[[Prototype]]
这也就是 所有自定义对象都继承自Object 的来源。