原型
[[Prototype]]
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
当你试图引用对象的属性时会触发[[Get]]操作,比如myObject.a
。对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了。
1 | var anotherObject = { |
使用for..in
遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到(并且是enumerable
)的属性都会被枚举。使用in
操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举):
1 | var anotherObject = { |
因此,当你通过各种语法进行属性查找时都会查找[[Prototype]]链,知道找到属性或者查找完整条原型链。
Object.prototype
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype
。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype
对象,所以它包含JavaScript
中许多通用的功能。
属性设置和屏蔽
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性。下面是完整的过程:
1 | myObject.foo = "bar"; |
- 如果
myObject
对象中包含名为foo
的普通数据访问属性,这条赋值语句会修改已有的属性值。 - 如果
foo
不是直接存在于,myObject
中,[[Prototype]]链就会被便利,类似[[Get]]操作。如果原型链上找不到foo
,foo
就会被直接添加到myObject上。 - 如果
foo
不直接存在于myObject
中而是存在于原型链上层时myObject.foo = "bar"
会出现三种情况:- 如果在[[Prototype]]链上层存在名为
foo
的普通数据访问属性并且没有被标记为只读(writable:false
),那么就会直接在myObject
中添加一个名为foo
的新属性,他是屏蔽属性。 - 如果在[[Prototype]]链上存在
foo
,但是它被标记为只读(writable:false
),那么无法修改已有的属性或者在myObject
上创建屏蔽属性。如果在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。 - 如果在[[Prototype]]链上存在
foo
并且它是一个setter
,那就一定会调用这个setter
。foo
不会被添加到(或者说屏蔽于)myObject
,也不会重新定义foo
这个setter
。
- 如果在[[Prototype]]链上层存在名为
如果你希望在第二种和第三种情况下也屏蔽foo
,那就不能用=
操作符来赋值,而是使用Object.defineProperty(..)
来向myObject
添加foo
。
“构造函数”
在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
看一段代码:
1 | function Foo(){ |
Foo.prototype
默认有一个公有并且不可枚举的属性.constructor
,这个属性引用的是对象关联的函数。此外,我们可以看到通过“构造函数”调用new Foo()
创建的对象也有一个.constructor
属性,指向“创建这个对象的函数”。
事实上a
本身并没有.constructor
属性。并且虽然a.construstor
确实指向Foo
函数,但是这个属性并不是表示a
由Foo
“构造”。a.construstor
只是通过默认的[[Prototype]]委托指向Foo
,这和“构造”毫无关系。思考以下代码:
1 | function Foo() { /* .. */ } |
由此可见,a1.constructor
是一个非常不可靠并且不安全的引用。
(原型)继承
下面这段代码使用的就是典型的“原型风格”:
1 | function Foo(name) { |
这段代码的核心部分就是语句Bar.prototype = Object.create(Foo.prototype)
。调用Object.create(..)
会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象。然后把原始的关联对象抛弃掉。
下面有两种常见的错误方法:
Bar.prototype = Foo.prototype
并不会创建一个关联到Bar.prototype
的新对象,它只是让Bar.prototype
直接引用Foo.prototype
对象。因此当你执行类似Bar.prototype.myLabel = ...
赋值语句时会直接修改Foo.prototype
对象本身。Bar.prototype = new Foo()
的确会创建一个关联到Bar.prototype
的新对象。但是它使用了Foo(..)
的“构造函数调用”,如果函数Foo
有一些副作用的话,就会影响到Bar()
的“后代”。
检查“类”关系
在传统的面向类环境中,检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为“内省”(或者反射)。
1 | function Foo(){ |
第一种方法是站在“类”的角度来判断:
1 | a instancsof Foo; // true |
这个方法只能处理对象(a)和函数之间的关系。不能判断两个对象之间是否通过[[Prototype]]链关联。
第二种方法:
1 | Foo.prototype.isPrototypeOf( a ); //true |
isPrototypeOf(..)
回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype
?
我们可以借此判断两个对象之间的关系:
1 | b.isPrototypeOf( c ); |
其他的还有:
1 | Object.getPrototypeOf( a ) === Foo.prototpe; //true |
虽然这些JavaScript机制和传统的面向类语言中的“类初始化”和“类继承”很相似,但是JavaScript中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的[[Prototype]]链关联的。