[[Prototype]]
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
[[Prototype]]引用有什么用呢?当开发者试图引用对象的属性时会触发[[Get]]操作,对于默认的[[Get]]操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。但是如果不存在于本身,就需要使用对象的[[Prototype]]链了。
对于默认的[[Get]]操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]]链:
let anotherObj = { a: 2 };
// 创建一个关联到anotherObj的对象
let myObj = Object.create(anotherObj);
console.log(myObj.a); // 2
显然myObj.a并不存在,但是尽管如此,属性访问仍然成功(在anotherObj中)找到了值2。
但是,如果anotherObj中也找不到a并且[[Prototype]]链不为空的话,就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]链。如果是后者的话,[[Get]]操作的返回值是undefined。
使用for..in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到并且是enumerable的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论是否枚举)。
let anotherObj = { a: 2 };
// 创建一个关联到anotherObj的对象
let myObj = Object.create(anotherObj);
for(let key in myObj){
console.log("found:" + key); // found: a
}
("a" in myObj); // true
Object.prototype
哪里是[[Prototype]]的尽头呢?
所有普通的[[Prototype]]链最终都会执行内置的Object.prototype ,由于所有“普通”对象都源于这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。比如.toString(),.valueOf()。
属性设置和屏蔽
myObj.foo = "bar";
如果myObj对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值。
如果foo不是直接存在于myObj中,[[Prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到foo,foo就会被直接添加到myObj上。
然而,如果foo存在于原型链上层,赋值语句myObj.foo = “bar”的行为就会有些不同。
如果属性名foo既出现在myObj中也出现在myObj的[[Prototype]]链上层,那么就会发生屏蔽。myObj中包含的foo属性会屏蔽原型链上层的所有foo属性,因为myObj.foo总是选择原型链中最底层的foo属性。
下面我们来分析一下如果foo不直接存在于myObj中,而是存在于原型链上层时myObj.foo = “bar”会出现的三种情况。
- 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性,并且没有被标记为只读(writable:false),那会直接在myObj中添加一个名为foo的新属性,它是屏蔽属性。
- 如果在[[Prototype]]链上层存在foo,并且它被标记为只读(writable: false),那么无法修改已有属性或者在myObj上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则这条赋值语句会被忽略。不会发生屏蔽。
- 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到myObj,也不会重新定义foo这个属性。
如果希望在第二种和第三种也屏蔽foo,那就不能用=操作符赋值,而是使用Object.defineProperty(..)来向myObj添加foo。
第二种情况,只读属性会阻止原型链下层隐式创建同名属性,这样做主要是为了模拟类属性的继承。原型链上层的foo看作是父类的属性,它会被myObj继承,这样一来myObj中foo也是只读,所以无法创建。
“类”
JavaScript中有一个奇怪的行为一直在被滥用,那就是模仿类。
这种奇怪的“类似类”的行为利用了函数的一种特殊特性: 所有的函数默认都会拥有一个名为prototype的共有并且不可枚举的属性,它会指向另一个对象。
function Foo(){
//
}
Foo.prototype; // {}
Foo.prototype通常被称为Foo的原型,那这个对象到底是什么?
直接的解释就是通过调用new Foo()创建的每个对象将最终被[[Prototype]]链接到这个“Foo.prototype”对象。
function Foo(){
}
let a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // true
调用new Foo()会创建a,其中一步就是将a内部的[[Prototype]]链接到Foo.prototype所指的对象。
new Foo()会生成一个新对象,这个新对象的内部链接[[Prototype]]关联的是Foo.prototype对象。
在JavaScript中,没有类的复制机制。只能创建多个对象,他们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会复制,因为这些对象之间并不完全失去联系,他们是互相关联的。
“构造函数”
function Foo(){
//
}
let a = new Foo();
为什么我们认为Foo是一个“类”呢?
因为我们看到了关键字new,在面向类的语言中构造类实例是也会用到它。另一个原因是,看起来我们执行了类的构造函数方法,Foo()的调用方式很像初始化类时类构造函数的调用方式。
function Foo(){
//
}
Foo.prototype.constructor === Foo; // true
let a = new Foo();
a.constructor === Foo; // true
Foo.prototype默认有一个共有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数(本例是Foo)。此外,我们可以看到通过“构造函数”调用new Foo()创建的对象也有一个constructor属性,指向“创建这个对象的函数”。
在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。
函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
“面向类”
function Foo(name){
this.name = name;
}
Foo.prototype.myName = function(){
return this.name;
}
let a = new Foo("a");
let b = new Foo("b");
a.myName(); // a
b.myName(); // b
以上代码是一种面向类的方式,this.name = name 给构造的对象添加了name属性,有点像类实例封装的数据值。
Foo.prototype.myName会给Foo.prototype对象添加属性,a,b的[[Prototype]]会关联到Foo.prototype上,当a,b找不到myName时,它会通过委托在Foo.prototype上找到。
回顾“构造函数”
在之前讨论.constructor属性时,看起来a.constructor === Foo为真意味着a确实有一个指向Foo的.constructor属性,但是不是这样。
.constructor引用同样委托给了Foo.prototype,而Foo.prototype.constructor默认指向了Foo。
把.constructor属性指向Foo看做是a对象由Foo“构造”这种说法很好理解,但其实是不对的,a.constructor只是通过默认的[[Prototype]]委托指向Foo,这个和“构造”毫无关系。
Foo.prototype的.constructor属性只是Foo函数在声明时的默认属性。如果新创建一个对象并替换函数的默认的.prototype对象引用,那么由new Foo()生成的新对象并不会获得.constructor属性。
function Foo(name){
this.name = name;
}
Foo.prototype = {};
let a = new Foo("a");
a.constructor === Foo; // false
a.constructor === Object; // true
那怎么修复由Foo构造的说法呢?我们可以手动给Foo.prototype添加一个.constructors属性。
function Foo(name){
this.name = name;
}
Foo.prototype = {}; // 创建一个新的原型对象
// 通过defineProperty修复丢失的.constructor属性
Object.defineProperty(Foo.prototype,'constructor',{
enumerable: false,
writable: true,
configurable: true,
value: Foo //让.constructor指向Foo
});
在JavaScript中.constructor并不表示对象被它构造。它不一定指向默认的函数引用,所以它是一个非常不可靠的并且不安全的引用。通常来说要尽量避免这些引用。
(原型)继承
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.myLabel = function(){
return this.label;
}
let a = new Bar("a","obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"
这段代码的核心部分就是语句Bar.prototype = Object.create(Foo.prototype)。调用Object.create(..)会凭空创建一个”新”对象并把新对象内部的[[Prototype]]关联到指定的对象(本例是Foo.prototype)。
声明function Bar(){..}时,和其他函数一样,Bar会有一个.prototype关联到默认的对象,但是这个对象并没有像我们想要的那样关联到Foo.prototype。因此我们创建一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。
在ES6之前,我们通过设置.proto属性来修改对象的[[Prototype]]关联,这个方法并不是标准并且无法兼容所有浏览器。ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。
// ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
// ES6 开始可以直接修改现有Bar.prototype
Object.setPrototypeOf(Bar.prototype,Foo.prototype);
检查“类”关系
假设有对象a,如何寻找对象a委托的对象呢?在传统的面向类环境中,检查一个实例的继承祖先通常被称为内省(或者反射)。
function Foo(){
// ..
}
let a = new Foo();
a instanceof Foo;
instanceof操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof回答的问题是:在a的整条[[Prototype]]原型链中是否有Foo.prototype指向的对象?但是这个方法只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系。无法判断两个对象之间是否通过[[Prototype]]链关联。
我们也可以直接获取一个对象的[[prototype]]链。在ES5中标准的方法是Object.getPrototypeOf(a)。
Object.getPrototypeOf(a) === Foo.prototype;
绝大多数浏览器也支持一种非标准的方法来访问内部[[prototype]]属性。
a.__proto__ === Foo.prototype; //true
这个属性引用了内部的[[Prototype]]对象,如果想直接查找,可以通过proto.proto…来遍历。
proto看起来很像一个属相,但实际上它更像一个getter/setter。
Object.defineProperty(Object.prototype, "__proto__",{
get: function(){
return Object.getPrototypeOf(this);
},
set: function(o){
Object.setPrototypeOf(this,o);
return o;
}
});
对象关联
[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。
通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。