0%

《你不知道的JavaScript》读书笔记(四)

原型

[[Prototype]]

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
当你试图引用对象的属性时会触发[[Get]]操作,比如myObject.a。对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了。

1
2
3
4
5
6
7
8
var anotherObject = {
a:2
};

//创建一个关联到anotherObject的对象
var myObject = Object.create( anotherObject );

myObject.a; //2

使用for..in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到(并且是enumerable)的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举):

1
2
3
4
5
6
7
8
9
10
11
12
13
var anotherObject = {
a:2
};

//创建一个关联到anotherObject的对象
var myObject = Object.create( anotherObject );

for(var k in myObject){
console.log("found: " + k);
}
// found: a

("a" in myObject); //ture

因此,当你通过各种语法进行属性查找时都会查找[[Prototype]]链,知道找到属性或者查找完整条原型链。

Object.prototype

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。

属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性。下面是完整的过程:

1
myObject.foo = "bar";
  • 如果myObject对象中包含名为foo的普通数据访问属性,这条赋值语句会修改已有的属性值。
  • 如果foo不是直接存在于,myObject中,[[Prototype]]链就会被便利,类似[[Get]]操作。如果原型链上找不到foofoo就会被直接添加到myObject上。
  • 如果foo不直接存在于myObject中而是存在于原型链上层时myObject.foo = "bar"会出现三种情况:
    • 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable:false),那么就会直接在myObject中添加一个名为foo的新属性,他是屏蔽属性
    • 如果在[[Prototype]]链上存在foo,但是它被标记为只读(writable:false),那么无法修改已有的属性或者在myObject上创建屏蔽属性。如果在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
    • 如果在[[Prototype]]链上存在foo并且它是一个setter,那就一定会调用这个setterfoo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter

如果你希望在第二种和第三种情况下也屏蔽foo,那就不能用=操作符来赋值,而是使用Object.defineProperty(..)来向myObject添加foo

“构造函数”

在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。

看一段代码:

1
2
3
4
5
6
7
8
function Foo(){
//...
}

Foo.prototype.constructor === Foo; //true;

var a = new Foo();
a.constructor === Foo; //ture

Foo.prototype默认有一个公有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数。此外,我们可以看到通过“构造函数”调用new Foo()创建的对象也有一个.constructor属性,指向“创建这个对象的函数”。
事实上a本身并没有.constructor属性。并且虽然a.construstor确实指向Foo函数,但是这个属性并不是表示aFoo“构造”。a.construstor只是通过默认的[[Prototype]]委托指向Foo,这和“构造”毫无关系。思考以下代码:

1
2
3
4
5
6
7
function Foo() { /* .. */ }

Foo.prototype = { /* .. */ }; //创建一个新原型对象

var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

由此可见,a1.constructor是一个非常不可靠并且不安全的引用。

(原型)继承

下面这段代码使用的就是典型的“原型风格”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Foo(name) {
this.name = name;
}

Foo.prototype.myName = function () {
return this.name;
};

function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
// 我们创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);

// 注意!现在没有Bar.prototype.constructor了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function () {
return this.label;
}

var a = new Bar("a", "obj a");

a.myName(); // "a""
a.myLabel(); // "obj a"

这段代码的核心部分就是语句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
2
3
4
5
6
7
function Foo(){
// ...
}

Foo.prototype.blah = ...;

var a = new Foo();

第一种方法是站在“类”的角度来判断:

1
a instancsof Foo; // true

这个方法只能处理对象(a)和函数之间的关系。不能判断两个对象之间是否通过[[Prototype]]链关联。

第二种方法:

1
Foo.prototype.isPrototypeOf( a ); //true

isPrototypeOf(..)回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype
我们可以借此判断两个对象之间的关系:

1
b.isPrototypeOf( c );

其他的还有:

1
2
3
Object.getPrototypeOf( a ) === Foo.prototpe; //true

a.__proto__ === Foo.prototype; //true

虽然这些JavaScript机制和传统的面向类语言中的“类初始化”和“类继承”很相似,但是JavaScript中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的[[Prototype]]链关联的。