JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引
用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。
var anotherObject = { a:2 }; // 创建一个关联到 anotherObject 的对象 var myObject = Object.create( anotherObject ); myObject.a; // 2
现在 myObject 对象的 [[Prototype]] 关联到了 anotherObject。显然 myObject.a 并不存在, 但是尽管如此,属性访问仍然成功地(在 anotherObject 中)找到了值 2。
但是,如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找 下去。
这个过程会持续到找到匹配的属性名或者查找完整条 [[Prototype]] 链。如果是后者的话, [[Get]] 操作的返回值是 undefined。
使用 for..in 遍历对象时原理和查找 [[Prototype]] 链类似,任何可以通过原型链访问到的属性都会被枚举。使用 in 操作符来检查属性在对象
中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举):
var anotherObject = { a:2 }; // 创建一个关联到 anotherObject 的对象 var myObject = Object.create( anotherObject ); for (var k in myObject) { console.log("found: " + k); } // found: a ("a" in myObject); // true
但是到哪里是 [[Prototype]] 的“尽头”呢?
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的“普通” (内置,不是特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为)
这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。
myObject.foo = "bar";
在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。
-
如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
-
如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
-
如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter
var anotherObject = { a:2 }; var myObject = Object.create( anotherObject ); anotherObject.a; // 2 myObject.a; // 2 anotherObject.hasOwnProperty( "a" ); // true myObject.hasOwnProperty( "a" ); // false myObject.a++; // 隐式屏蔽! anotherObject.a; // 2 myObject.a; // 3 myObject.hasOwnProperty( "a" ); // true
尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a
function Foo() { // ... } Foo.prototype; // { }
这个对象通常被称为 Foo 的原型,因为我们通过名为 Foo.prototype 的属性引用来访问它。 然而不幸的是,这个术语对我们造成了极大的误导,稍后我们就会看到。如果是我的话就 会叫它“之前被称为 Foo 的原型的那个对象”。
最直接的解释就是,这个对象是在调用new Foo()时创建的,最后会被(有点武断地)关联到这个“Foo.prototype”对象上。
我们来验证一下:
function Foo() { // ... } var a = new Foo(); Object.getPrototypeOf( a ) === Foo.prototype; // true
调用new Foo()时会创建a,其中一步就是将a内部的 [[Prototype]] 链接到 Foo.prototype 所指向的对象。
实际上,绝大多数 JavaScript 开发者不知道的秘密是,new Foo() 这个函数调用实际上并没 有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目 标:一个关联到其他对象的新对象。
function Foo() { // ... } var a = new Foo();
以上的这个构造函数看起来很像一个类。其实它就相当与是调用执行了一个函数,只不过是前面加了new
function Foo() { // ... } Foo.prototype.constructor === Foo; // true var a = new Foo(); a.constructor === Foo; // true
实际上 a 本身并没有 .constructor 属性。而且,虽然 a.constructor 确实指 向 Foo 函数,但是这个属性并不是表示 a 由 Foo“构造”
实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当 你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数 调用”。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。
关于constructor,通常我们可能理解成,a.constructo指的是a的构造函数是谁。
其实这是错误的,不妨把它看作是普通的一个属性,也是沿着原型链查找而已。
function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 创建一个新原型对象 var a1 = new Foo(); a1.constructor === Foo; // false! a1.constructor === Object; // true!
foo本身的原型被重新赋值了,导致原型链断裂,所以并不是指向foo,而是一直查找到了顶端,指向了Object.prototype,它的上面刚好有constructor属性,一个内置的object函数。
所以日常不要直接写成foo.prototype = {//...},可以写成foo.prototype.fn = function(){},采取这种单一赋值方法。
当然,也有弥补的办法,如下:
function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 创建一个新原型对象 // 需要在 Foo.prototype 上“修复”丢失的 .constructor 属性 Object.defineProperty( Foo.prototype, "constructor" , { enumerable: false, writable: true, configurable: true, value: Foo // 让 .constructor 指向 Foo } );
或者是在重写foo.ptototype时候重新声明一下。
foo.prototype = { constructor:Foo, //...... }
实际上,对象的 .constructor 会默认指向一个函数,这个函数可以通过对象的 .prototype 引用。“constructor”和“prototype”这两个词本身的含义可能适用也可能不适用。最好的 办法是记住这一点“constructor 并不表示被构造”。