通过代码示例一步步了解原型&原型链

275 阅读5分钟

基本概念

什么是原型呢?

初始化一个构造函数,它有一个 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__: ƒ, …}

在浏览器控制台展开看看

image.png

这一眼看过去,好像也不知道是什么,但你是否记得上面提到过的:

每个原型对象都有一个 constructor 属性指向关联的构造函数

我们访问原型的原型的 constructor 属性试试

console.log(person1.__proto__.__proto__.constructor)    // ƒ Object()

没想到吧,实例对象原型的原型就是 Object 的原型对象

那么 person1 对象原型的原型的原型呢?

console.log(person1.__proto__.__proto__.__proto__)  // null

到头了,没有像我们想象中的那样无限循环下去。我们可以得出一个结论

原型的终点是 null

借用冴羽老师的一张图就清楚其中的关系了:

image.png

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

用接地气的话说就是,找 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 的显示原型,

最后放上原型链终极图解:

image.png

总结

  • 原型的本质其实就是一个对象
  • 原型的作用是给其他对象提供共享属性,即用来实现基于原型的继承与属性的共享
  • 相互关联的原型组成的链状结构就是原型链
  • 原型链的长度是有限的,且最终一定指向 null
  • 所有原型对象的根原型对象是 Object 原型对象, 它被内置在 Object.prototype 属性上