原型的深度认识(一)

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" 会出现的三种情况。

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。

  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。

  3. 如果在[[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 并不表示被构造”。  

猜你喜欢

转载自www.cnblogs.com/hjj2ldq/p/9315735.html