聊聊我对原型和原型链的理解

69 阅读5分钟

基本概念

总的来说: 原型和原型链其实是js专门为了实现”继承“而设计的。

原型

按照TC39标准的约定:

先明确,[[Prototype]] 和 prototype 是两个东西,并且不需要理解为什么是这样。 标准!背下来就行!

[[Prototype]] : 所有对象都有一个叫做 [[Prototype]] 的内部属性,此属性的值是null或指向一个对象。并且它用于实现继承,当读取对象的某个属性时,如果在对象本身读取不到,就会尝试从这个[[Prototype]]中读取。

prototype : 所有的函数创建时,都会创建一个普通对象,作为他的prototype属性,当此函数被当做构造函数用来构造一个对象后,构造出的对象的[[Prototype]]会默认指向这个函数的prototype属性。

而我们常说的__proto__ 其实是浏览器对 [[Prototype]] 的实现,因为比较常见,所以后来大家普遍喜欢用__proto__直接表示 [[Prototype]],所以我也会在后面的描述中直接以__proto__来表示原型。

原型链

然后再说原型链 由标准得知:

  1. 每个对象都有一个原型__proto__,每个函数都有一个prototype属性,且对象的__proto__会指向它的构造函数的prototype属性。
  2. 对象在读取某个属性时,如果在对象本身读取不到,就会尝试从这个__proto__中读取。
  3. 函数的prototype属性默认是一个”普通对象“,就相当于是通过new Object()创建的,所以函数的 prototype._proto_ 则指向Object函数的prototype,且prototype也是对象,在读取不到属性时会去自己的__proto__中读取。
  4. Object.prototype.__proto__ === null

综合上面几点得出结论:

对象的__proto__会指向构造函数的prototype。构造函数的prototype对象内的__proto__会指向Object的prototype,而Object的prototype则指向null。

而js设定了一个读取规则,当我在对象内读取一个不存在的属性时,会沿着这个路径依次查找读取。 这个读取路径的表现和链表这个数据结构很像,所以我们将其称为 原型链

代码辅助理解

 function Person(name, age){ 
    this.name = name;
    this.age = age;
 }
 
let suixue = new Person('碎雪', 18)

console.log(suixue.__proto__ === Person.prototype)             // true
console.log(suixue.__proto__.__proto__ === Object.prototype)   // true
console.log(suixue.__proto__.__proto__.__proto__)              // null

辅助理解:
现在,我要看看碎雪有钱没,访问suixue.money,

  1. 先在suixue里找,发现没有money
  2. 找不到我得再去 suixue._proto_ 里去找,发现还没有money
  3. 还找不到再去 suixue._proto_._proto_ 里去找,还没有money
  4. 再去 suixue._proto_._proto_._proto_ 里去找,这个值是个null,既没有money,又没有__proto__,现在终于知道了,碎雪没钱。

拓展内容

拓展内容,可以简单了解一下

其实这样说下来基本上就已经将原型和原型链聊透了。 但是有看过很多小伙伴还会说:”不对吧?还有constructor啊,你这块怎么不说?“ 说实话,constructor 和 原型 真的没啥关系!

这里贴一下TC39标准,他真的只是一个额外产物,仅是函数的prototype中的一个指向函数本身的指针,没有任何额外的作用。

TC39关于Object.prototype.constructor的描述

可能会用到的唯一场景就是,当你使用一个第三方库构建出一个对象,发现原始函数需要修改,你可以尝试用这种方法。但是绝不推荐!

什么?你还不信?来来来,咱好好聊聊!

原型模式的发展历程

除了TC39标准之外,从原型模式本身的发展来看,也和constructor没有啥关系。

原型模式本身就是创建型设计模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”。

最原始的原型模式实现:在构造时复制,每次构造时会开辟同等大小的内存空间。这虽然使得 obj1、obj2 与它们的原型完全一致,但也非常地不经济 —— 内存空间的消耗会急速增加。

大家一看这也太浪费了,然后做了一个优化,在写时复制。我们只要在系统中指明 obj1 和 obj2 等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。

但当需要写对象(例如 obj2)的属性时,我们就复制一个出来。 不过对于经常写操作的系统来说,这种法子并不比上一种法子经济,同样是空间浪费。

最后大家想出一个好办法 —— 设定读取规则

  1. 保证写入实例的属性在读取时被首先访问到。
  2. 在对象中没有指定的属性时,则尝试遍历对象的整个原型链,直到原型为空(null) 或找到该属性。 设定这两个规则后,读取属性时最深可以读到原型,写则只在自己的内存里写,非常合理! 这其实就是js引擎实现的原型机制。

好了,大家看完了原型的发展历程,现在相信constructor跟原型确实没啥关系了吧?

大佬说了,constructor是js语言设计的历史遗留物

知乎这里有个同学也有同样的疑问: JavaScript 中对象的 constructor 属性的作用是什么? 大家可以进去看看贺师俊大佬的回答。

OK,聊了很多有的没的,现在大家一定看会了吧!面试问到一定没问题了吧!