5.1 [[Prototype]]
JavaScript 中的对象有一个特殊的 [[Prototype]]
内置属性,其实就是对于其他对象的引用。
- 几乎所有的对象在创建时
[[Prototype]]
属性都会被赋予一个非空的值。
js
var anotherObject = {
a: 2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create(anotherObject);
for (var k in myObject) {
console.log("found: " + k);
}
// found: a
console.log(("a" in myObject)); // true
5.1.1 Object.prototype
[[Prototype]]
的"尽头"
所有普通的 [[Prototype]]
链最终都会指向内置的 Object.prototype。
5.1.2 属性设置和屏蔽
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值
js
myObject.foo = "bar";
上面代码的过程
- 如果 myObject 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有的属性值。
- 如果 foo 不是直接存在于 myObject 中,
[[Prototype]]
链就会被遍历,类似[[Get]]
操作。 - 如果原型链上找不到 foo,foo 就会被直接添加到 myObject 上。
- 如果属性名 foo 既出现在 myObject 中也出现在 myObject 的
[[Prototype]]
链上层,那么就会发生屏蔽。
隐式屏蔽
js
var anotherObject = {
a: 2
};
var myObject = Object.create(anotherObject);// 2 2
console.log(myObject.a, anotherObject.a);
console.log(anotherObject.hasOwnProperty("a"));//true
console.log(myObject.hasOwnProperty("a"));//false
myObject.a++;// 隐式屏蔽
console.log(myObject.a, anotherObject.a);//3 2
console.log(myObject.hasOwnProperty("a"));//true
++ 操作相当于 myObject.a = myObject.a + 1。
- 因此 ++ 操作首先会通过
[[Prototype]]
查找属性 a 并从 anotherObject.a 获取当前属性值 2 - 然后给这个值加 1
- 接着用
[[Put]]
将值 3 赋给 myObject 中新建的屏蔽属性 a
5.2 "类"
5.2.1 "类"函数
模仿类
这种奇怪的"类似类"的行为利用了函数的一种特殊特性:
所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象. 这个对象被称为函数的原型
js
function Foo() {
//
}
console.log(Foo.prototype);//{} Foo 的原型
var a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype);//true
var b = new Foo();
console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b));//true
调用 new Foo() 时会创建 a,其中的一步就是给 a 一个内部的 [[Prototype]]
链接,关联到 Foo.prototype 指向的那个对象
- 在 JavaScript 中,并没有类似的复制机制。
- 你不能创建一个类的多个实例,只能创建多个对象,它们
[[Prototype]]
关联的是同一个对象。 - 但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。
5.2.2 "构造函数"
在 JavaScript 中对于"构造函数"最准确的解释是,所有带 new 的函数调用。
- 函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成"构造函数调用"。
js
function Foo() {
}
console.log(Foo.prototype.constructor === Foo);//true
var a = new Foo();
console.log(a.constructor === Foo);//true
- Foo.prototype 默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数。
- 到通过"构造函数"调用 new Foo() 创建的对象也有一个 .constructor 属性,指向"创建这个对象的函数"。
- .constructor 引用同样被委托给了 Foo.prototype,而Foo.prototype.constructor 默认指向 Foo。
5.2.3 技术
模仿类的行为
js
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
var a = new Foo( "a" );
var b = new Foo( "b" );
a.myName(); // "a"
b.myName(); // "b"
a1 并没有 .constructor 属性,所以它会委托 [[Prototype]]
链上的 Foo.prototype。
但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性!),所以它会继续委托, 这次会委托给委托链顶端的 Object.prototype。
这个对象有 .constructor 属性,指向内置的 Object(..) 函数。
js
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
手动添加.constructor 属性
js
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
// 需要在 Foo.prototype 上"修复"丢失的 .constructor 属性
// 新对象属性起到 Foo.prototype 的作用
// 关于 defineProperty(..),参见第 3 章
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
} );
5.3 (原型)继承
原型风格
- 要创建一个合适的关联对象,我们必须使用 Object.create(..)
js
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
//调用Object.create(..) 会凭空创建一个"新"对象并把新对象内部的 [[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"
是常见的错误做法
js
// 和你想要的机制不一样!
//不会创建一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接引用 Foo.prototype 对象。
//因此当你执行类似 Bar.prototype.myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象本身。
Bar.prototype = Foo.prototype;
// 基本上满足你的需求,但是可能会产生一些副作用 :(
//的确会创建一个关联到 Bar.prototype 的新对象。但是它使用了 Foo(..) 的"构造函数调用",如果函数 Foo 有一些副作用(比如写日志、修改状态、注册到其他对象、给 this 添加数据属性,等等)的话,就会影响到 Bar() 的"后代",后果不堪设想。
Bar.prototype = new Foo();
修改对象的 [[Prototype]] 关联
辅助函数 Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。
js
// ES6 之前需要抛弃默认的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 开始可以直接修改现有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
检查"类"关系
检查一个实例(JavaScript 中的对象)的继承祖先(JavaScript 中的委托关联)通常被称为内省(或者反射)。
instanceof 操作符
- 在 a 的整条
[[Prototype]]
链中是否有指向 Foo.prototype 的对象 - 只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系
第二种判断 [[Prototype]]
反射的方法
js
Foo.prototype.isPrototypeOf( a ); // true