基本概念
总的来说: 原型和原型链其实是js专门为了实现”继承“而设计的。
原型
按照TC39标准的约定:
先明确,[[Prototype]] 和 prototype 是两个东西,并且不需要理解为什么是这样。 标准!背下来就行!
[[Prototype]] : 所有对象都有一个叫做 [[Prototype]] 的内部属性,此属性的值是null或指向一个对象。并且它用于实现继承,当读取对象的某个属性时,如果在对象本身读取不到,就会尝试从这个[[Prototype]]中读取。
prototype : 所有的函数创建时,都会创建一个普通对象,作为他的prototype属性,当此函数被当做构造函数用来构造一个对象后,构造出的对象的[[Prototype]]会默认指向这个函数的prototype属性。
而我们常说的__proto__ 其实是浏览器对 [[Prototype]] 的实现,因为比较常见,所以后来大家普遍喜欢用__proto__直接表示 [[Prototype]],所以我也会在后面的描述中直接以__proto__来表示原型。
原型链
然后再说原型链 由标准得知:
- 每个对象都有一个原型__proto__,每个函数都有一个prototype属性,且对象的__proto__会指向它的构造函数的prototype属性。
- 对象在读取某个属性时,如果在对象本身读取不到,就会尝试从这个__proto__中读取。
- 函数的prototype属性默认是一个”普通对象“,就相当于是通过
new Object()
创建的,所以函数的 prototype._proto_ 则指向Object函数的prototype,且prototype也是对象,在读取不到属性时会去自己的__proto__中读取。 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,
- 先在suixue里找,发现没有money
- 找不到我得再去 suixue._proto_ 里去找,发现还没有money
- 还找不到再去 suixue._proto_._proto_ 里去找,还没有money
- 再去 suixue._proto_._proto_._proto_ 里去找,这个值是个null,既没有money,又没有__proto__,现在终于知道了,碎雪没钱。
拓展内容
拓展内容,可以简单了解一下
其实这样说下来基本上就已经将原型和原型链聊透了。 但是有看过很多小伙伴还会说:”不对吧?还有constructor
啊,你这块怎么不说?“
说实话,constructor
和 原型 真的没啥关系!
这里贴一下TC39标准,他真的只是一个额外产物,仅是函数的prototype中的一个指向函数本身的指针,没有任何额外的作用。
可能会用到的唯一场景就是,当你使用一个第三方库构建出一个对象,发现原始函数需要修改,你可以尝试用这种方法。但是绝不推荐!
什么?你还不信?来来来,咱好好聊聊!
原型模式的发展历程
除了TC39标准之外,从原型模式本身的发展来看,也和constructor
没有啥关系。
原型模式本身就是创建型设计模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”。
最原始的原型模式实现:在构造时复制,每次构造时会开辟同等大小的内存空间。这虽然使得 obj1、obj2 与它们的原型完全一致,但也非常地不经济 —— 内存空间的消耗会急速增加。
大家一看这也太浪费了,然后做了一个优化,在写时复制。我们只要在系统中指明 obj1 和 obj2 等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。
但当需要写对象(例如 obj2)的属性时,我们就复制一个出来。 不过对于经常写操作的系统来说,这种法子并不比上一种法子经济,同样是空间浪费。
最后大家想出一个好办法 —— 设定读取规则:
- 保证写入实例的属性在读取时被首先访问到。
- 在对象中没有指定的属性时,则尝试遍历对象的整个原型链,直到原型为空(null) 或找到该属性。 设定这两个规则后,读取属性时最深可以读到原型,写则只在自己的内存里写,非常合理! 这其实就是js引擎实现的原型机制。
好了,大家看完了原型的发展历程,现在相信constructor
跟原型确实没啥关系了吧?
大佬说了,constructor是js语言设计的历史遗留物
知乎这里有个同学也有同样的疑问: JavaScript 中对象的 constructor 属性的作用是什么? 大家可以进去看看贺师俊大佬的回答。
OK,聊了很多有的没的,现在大家一定看会了吧!面试问到一定没问题了吧!