基本概念
什么是原型呢?
初始化一个构造函数,它有一个 prototype 属性,该属性值指向的就是构造函数的原型
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype) // {constructor: ƒ}
比如上文的Person.prototype指向的就是Person的原型,多么朴实无华
当然了,只有构造函数才有 prototype 这个属性,其他对象是没有的
我们来看看原型的定义
原型本质是一个对象,原型对象是为了给其他实例对象提供共享属性的
该如何理解这句话呢?不多说,直接 show code
访问原型对象
首先通过构造函数初始化一个实例对象
const person1 = new Person('Jason', 21)
console.log(person1) // Person {name: 'Jason', age: 21}
可以看到 person1 只具有构造函数中初始化的两个默认属性 name 和 age
上面说到构造函数可以通过prototype属性访问到原型对象,那么这个构造函数的实例对象如何访问到这个原型对象呢?
我们可以使用__proto__属性
console.log(person1.__proto__ === Person.prototype) // true
__proto__最初是由各个浏览器厂商独立实现的 hack 方法,目前已收入到ES6规范中,可以放心使用
当然,更推荐的还是使用 ES6 规定的获取原型的方法Object.getPrototypeOf()
console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
反过来,如何通过原型对象访问到构造函数呢?原型对象有个 constructor 属性:
console.log(Person.prototype.constructor === Person) // true
每个原型对象都有一个 constructor 属性指向关联的构造函数
共享属性
如果我们访问一个构造函数没有定义过的属性会怎么样?
console.log(person1.power) // undefined
当然是 undefined 了,没定义就没有咯
那我们尝试在原型对象上定义它,再次记住原型对象的作用:给实例对象提供共享属性
Person.prototype.power = 'god'
OK,我们尝试在实例对象上访问这个属性
console.log(person1.power) // god
成功了!
那么 person1 会有什么变化吗?打印下看看
console.log(person1) // Person {name: 'Jason', age: 21}
person1 是没有 power 属性,因为它并没有在 person1 或者构造函数中定义过
那 person1.power的值哪来的?答案已经呼之欲出
从 person1 的原型对象上拿到的
person1.power === person1.__proto__.power
所以我们可以得出第一个结论,当找不到对象的某个属性时,它会继续往这个对象的原型对象上找
我们再用这个构造函数初始化一个对象
const person2 = new Person('Marry', 22)
看一看这个 person2 长啥样
console.log(person2) // Person {name: 'Marry', age: 22}
person2 同样也没有 power 属性
console.log(person2.power) // god
虽然没有这个属性,但是也能访问到原型对象上的共享属性 power
console.log(person2.__proto__.power === person2.power) // true
没错,这俩个就是完全等价的!
我们通过 person2 改变这个绑定在原型上的属性值试试
person2.__proto__.power = 'godness'
console.log(person2.power) // godness
我们再通过 person1 访问呢?
console.log(person1.power) // godness
可以看到 person1 访问的也变了
也就是原型对象上的 power 属性是全部实例对象所共享的,一有俱有,一改俱改
这样我们应该就理解了,原型对象是用来提供共享属性这句话了
原型链?
我们知道了访问对象的某个属性当找不到时,会往它的原型对象上找,如果原型对象上也找不到呢?
首先看看 person1 原型对象目前长啥样
console.log(person1.__proto__) // {power: 'god', constructor: ƒ}
我们应该可以猜到,原型对象也是对象,在原型对象上没找到,那就继续往原型对象的原型对象上去找!
我们来看看原型的原型是什么
console.log(person1.__proto__.__proto__)
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
在浏览器控制台展开看看
这一眼看过去,好像也不知道是什么,但你是否记得上面提到过的:
每个原型对象都有一个 constructor 属性指向关联的构造函数
我们访问原型的原型的 constructor 属性试试
console.log(person1.__proto__.__proto__.constructor) // ƒ Object()
没想到吧,实例对象原型的原型就是 Object 的原型对象
那么 person1 对象原型的原型的原型呢?
console.log(person1.__proto__.__proto__.__proto__) // null
到头了,没有像我们想象中的那样无限循环下去。我们可以得出一个结论
原型的终点是 null
借用冴羽老师的一张图就清楚其中的关系了:
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
用接地气的话说就是,找 person1 对象某个属性没找到,就通过__proto__访问原型一层一层地找,直到找到这个属性或者到 null 位置结束,相互关联的原型就构成了原型链
扩展
通过上面的知识我们知道了
function Person() {
}
const person = new Person()
Person.prototype === person.__proto__ // true
构造函数 Person 的prototype也被称为显式原型,实例对象 person 的__proto__也被称为隐式原型
即构造函数的显式原型和实例对象的隐式原型是同一个对象
那么构造函数的隐式原型__proto__指向什么呢?
指向Function的原型,即
Person.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
所以原型链就串起来了:
构造函数的隐式原型指向 Function 的显示原型,
构造函数的显示原型的隐式原型指向 Object 的显示原型,
最后放上原型链终极图解:
总结
- 原型的本质其实就是一个对象
- 原型的作用是给其他对象提供共享属性,即用来实现基于原型的继承与属性的共享
- 相互关联的原型组成的链状结构就是原型链
- 原型链的长度是有限的,且最终一定指向
null - 所有原型对象的根原型对象是
Object 原型对象, 它被内置在Object.prototype属性上