深入 JavaScript 原型到原型链
作者:Seiya
时间:2019年06月18日
前言
JavaScript 不包含传统的类继承模型,而是使用 prototypal 原型模型。虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。
由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链。
原型
每一个 JavaScript 对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
[[Prototype]]
在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— [[Prototype]]
,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]]
属性都会被赋予一个非空的值。
[[Prototype]]
属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型。
注意:
对象的 [[Prototype]]
链接可以为空,虽然很少见。
[[__proto__]]
每一个 JavaScript 对象(除了null)都具有的一个属性,叫 [[__proto__]]
,这个属性会指向该对象的原型。
举个例子:
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
关系图如下所示:
补充:
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。
[[constructor]]
每个原型都有一个 constructor
属性指向关联的构造函数。
举个例子:
function Person() {}
console.log(Person === Person.prototype.constructor); // true
关系图如下所示:
实例与原型
每当你试图引用对象的属性时会触发 [[Get]]
操作,对于默认的 [[Get]]
操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。但是如果无法在对象本身找到需要的属性,就会继续访问对象的 [[Prototype]]
链。
这个过程会持续到找到匹配的属性名或者查找完整的 [[Prototype]]
链,如果未找到,[[Get]]
操作的返回值是 undefined 。
注意:
如果包含 Proxy 的话,这里对 [[Get]]
和 [[Put]]
的讨论就不适用了。
使用 for..in 遍历对象时原理和查找 [[Prototype]]
链类似,任何可以通过原型链访问到(并且是 enumerable)的属性都会被枚举。使用 in 操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举 )。
原型的原型
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 [[__proto__]]
指向构造函数的 [[prototype]]
,所以我们再更新下关系图:
原型链
原型链是一个由对象组成的有限对象链用于实现继承和共享属性。
最后一张关系图也可以更新为:
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
总结
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。