[[Prototype]]
JavaScript中的對象有一個特殊的[[Prototype]]內置屬性,其實就是對于其他對象的引用。幾乎所有的對象在創建時[[Prototype]]屬性都會被賦予一個非空的值。
var myObject = {
a: 2
};
myObject.a; // 2
[[Prototype]]引用由什么用呢?當你試圖引用對象的屬性時會觸發[[Get]]操作,比如myObject.a。對于默認的[[Get]]操作來說,第一步是檢查對象本身是都有這個屬性,如果有的話就使用它。但是如果a不在myObject中,就需要使用對象的[[Prototype]]鏈了。
對于默認的[[Get]]操作來說,如果無法在對象本身找到需要的屬性,就會繼續訪問對象的[[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。
Object.prototype
所有普通的[[Prototype]]鏈最終都會指向內置的Object.prototype。由于所有的“普通”對象都“源于”(或者說把[[Prototype]]鏈的頂端設置為)這個Object.prototype對象,所有它包含JavaScript中許多通用的功能。
屬性設置和屏蔽
給一個對象設置屬性并不僅僅是添加一個新屬性或者修改已有的屬性值。
myObject.foo = "bar";
如果myObject對象中包含名為foo的普通數據訪問屬性,這條賦值語句只會修改已有的屬性值。
如果foo不是直接存在于myObject中,[[Prototype]]鏈就會被遍歷,類似[[Get]]操作。如果原型鏈上找不到foo,foo就會被直接添加到myObject上。
然而,如果foo存在于原型鏈上層,賦值語句myObject.foo="foo"的行為就會有些不同。
如果屬性名foo既出現在myObject中也出現在myObject的[[Prototype]]鏈上層,那么就會發生屏蔽。myObject中包含的foo屬性會屏蔽原型鏈上層的所有foo屬性,因為myObject.foo總是會選擇原型鏈中最低層的foo屬性。
屏蔽比我們想想中更加復雜。下面分析下如果foo不直接存在于myObject中而是存在于原型鏈上層時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。
如果你希望在第二種和第三中情況下也屏蔽foo,那就不能使用=操作符來賦值,而是使用Object.defineProperty(..)來向myObject添加foo。
有些情況下回隱式產生屏蔽,一定要當心:
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。
修改委托屬性時一定要小心。如果想讓anotherObject.a的值增加,唯一的辦法是anotherObject.a++。
“類函數”
JavaScript中有一種奇怪的行為一直在被無恥地濫用,那就是模仿類。這種奇怪的“類似類”的行為利用了函數的一種特殊特性:所有的函數默認都會擁有一個名為Prototype的共有并且不可枚舉的屬性,它會指向另一個對象:
function Foo() {
// ...
}
Foo.prototype; // { }
這個對象通常被稱為Foo的原型,因為我們通過名為Foo.Prototype的屬性引用來訪問它。這個對象到底是什么?最直接的解釋就是,這個對象是在調用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()只是間接完成了我們的目標:一個關聯到其他對象的新對象。
繼承意味著復制操作,JavaScript(默認)并不會復制對象屬性。相反,JavaScript會在兩個對象之間創建一個關聯,這樣一個對象就可以通過委托訪問另一個對象的屬性和函數。委托這個術語可以更加準確滴描述JavaScript中對象的關聯機制。還有個偶爾會用到的JavaScript術語差異繼承。基本原則是在描述對象行為時,使用其不同于普遍描述的特性。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype默認(在代碼中第一行聲明時!)有一個公有并且不可枚舉的屬性.constructor,這個屬性引用的是對象關聯的函數。此外,可以看到通過“構造函數”調用new Foo()創建的對象也有一個.constructor屬性,指向“創建這個對象的函數”。
上一段代碼很容易讓人認為Foo是一個構造函數,因為我們使用new來調用它并且看到它“構造”了一個對象。實際上,Foo和你程序中的其他函數沒有任何區別。函數本身并不是構造函數,然而,當你在普通的函數調用前面加上new關鍵字之后,就會把這個函數調用變成一個“構造函數調用”。實際上,new會劫持所有普通函數并用構造對象的形式來調用它。換句話說,在JavaScript中對于“構造函數”最準確的解釋是,所有帶new的函數調用。
技術
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"
在這段代碼中,看起來似乎創建a和b時會把Foo.prototype對象復制到這兩個對象中,然而事實并不是這樣。在前面介紹默認[[Get]]算法時介紹過[[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。
Foo.prototype的.constructor屬性只是Foo函數在聲明時的默認屬性。如果你創建了一個新對象并替換了函數默認的.prototype對象引用,那么新對象并不會自動獲得.constructor屬性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
a1并沒有.constructor屬性,所以它會委托[[Prototype]]鏈上的Foo.prototype。但是這個對象也沒有.constructor屬性(不過默認的Foo.prototype對象有這個屬性!),所以它會繼續委托,這次會委托給委托鏈頂端的Object.prototype。這個對象有.constructor屬性,指向內置的Object(..)函數。
當然,你可以給Foo.prototype添加一個.constructor屬性,不過這需要手動添加一個符合正常行為的不可枚舉屬性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象
// 需要在Foo.prototype 上“修復”丟失的.constructor 屬性
// 新對象屬性起到Foo.prototype 的作用
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 讓.constructor 指向Foo
});
原型風格
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]]關聯到你指定的對象(本例中是Foo.prototype)。換句話說,這條語句的意思是:“創建一個新的Bar.prototype對象并把它關聯到Foo.prototype”。
注意:下面這兩種方式是常見的錯誤做法,實際上它們都存在一些問題:
// 和你想要的機制不一樣!
Bar.prototype = Foo.prototype;
// 基本上滿足你的需求,但是可能會產生一些副作用 :(
Bar.prototype = new Foo();
Bar.prototype=Foo.prototype并不會創建一個關聯到Bar.prototype的新對象,它只是讓Bar.prototype直接引用Foo.prototype對象。因此當你執行類似Bar.prototype.
myLabel = ...的賦值語句時會直接修改Foo.prototype對象本身。顯然這不是你想要的結果,否則你根本不需要Bar對象,直接使用Foo就可以了,這樣代碼也會更簡單一些。
Bar.prototype = new Foo()的確會創建一個關聯到Bar.prototype的新對象。但是它使用了Foo(..)的“構造函數調用”,如果函數Foo有一些副作用(比如寫日志、修改狀態、注冊到其他對象,給this添加數據屬性,等等)的話,就會影響到Bar()的“后代”,后果不堪設想。
因此,要創建一個合適的關聯對象,我們必須使用Object.create(..)而不是使用具有副作用的Foo(..)。這樣做唯一的缺點就是需要創建一個新對象然后把舊對象拋棄掉,不能直接修改已有的默認對象。
如果能有一個標準并且可靠的方法來修改對象的[[Prototype]]關聯就好了。在ES6之前,我們只能通過設置.proto屬性來實現,但是這個方法并不是標準并且無法兼容所有瀏覽器。ES6添加了輔助函數Object.setPrototypeOf(..),可以用標準并且考考的方法來修改關聯。
對比下兩種把Bar.prototype關聯到Foo.prototype的方法:
// ES6 之前需要拋棄默認的Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開始可以直接修改現有的Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
如果忽略掉Object.create(..)方法帶來的輕微性能損失(拋棄的對象需要進行垃圾回收),它實際上比ES6及其之后的方法更短并且可讀性更高。不過無論如何,這是兩種完全不同的語法。
檢查“類”關系
假設有對象a,如何找到對象a委托的對象(如果存在的話)呢?在傳統的面相類環境中,檢查一個實例(JavaScript中的對象)的繼承(JavaScript中的委托關聯)通常被稱為內省(或者反射)。
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
我們如何通過內省找出a的“祖先”(委托關聯)呢?第一種方法是站在“類”的角度來判斷:a instanceof Foo;//true
instanceof操作符的左操作數是一個普通的對象,右操作數是一個函數。instanceof回答的問題是:在a的整條[[Prototype]]鏈中是否有指向Foo.prototype的對象?
可惜,這個方法只能處理對象(a)和函數(帶.prototype引用的Foo)之間的關系。如果你想判斷兩個對象(比如a和b)之間是否通過[[Prototype]]鏈關聯,只用instanceof無法實現。
第二種判斷[[Prototype]]反射的方法,它更加簡潔:
Foo.prototype.isPrototypeOf( a ); // true
//在a的整條[[Prototype]]鏈中是否出現過Foo.prototype?
// 非常簡單:b 是否出現在c 的[[Prototype]] 鏈中?
b.isPrototypeOf( c );
我們也可以直接獲取一個對象的[[Prototype]]鏈。在ES5中,標準的方法是:```JavaScript
Object.getPrototypeOf(a)
可以驗證一下,這個對象引用是否和我們想的一樣:
```JavaScript
Object.getPrototypeOf( a ) === Foo.prototype; // true
絕大多數(不是所有!)瀏覽器也支持一種非標準的方法來訪問內部[[Prototype]]屬性:
a.__proto__ === Foo.prototype; // true
這個奇怪的.__proto__
(在ES6之前并不是標準!)屬性“神奇地”引用了內部的[[Prototype]]對象,如果你想直接查找(甚至可以通過.__proto__.__ptoto__...
來遍歷)原型鏈的話,這個方法非常有用。
和我們之前說過的.constructor一樣,.proto實際上并不存在于你正在使用的對象中(本例中是a)。實際上,它和其他的常用函數(.toString()、.isPrototypeOf(..),等等)一樣,存在于內置的Object.prototype中,它們是不可枚舉的。
.proto看起來很像一個屬性,但是實際上它更像一個getter/setter。.proto的實現大致上是這樣的:
Object.defineProperty(Object.prototype, "__proto__", {
get: function () {
return Object.getPrototypeOf(this);
},
set: function (o) {
// ES6 中的setPrototypeOf(..)
Object.setPrototypeOf(this, o);
return o;
}
});
因此,訪問(獲取值)a.proto時,實際上是調用了a.proto()(調用getter函數)。雖然getter函數存在于Object.prototype對象中,但是它的this指向對象a(this的綁定規則),所以和Object.getPrototypeOf(a)結果相同。
對象關聯
[[Prototype]]機制就是存在于對象中的一個內部鏈接,它會引用其他對象。
通常來說,這個鏈接的作用是:如果在對象上沒有找到需要的屬性或者方法引用,引擎就會繼續在[[Prototype]]關聯的對象上進行查找。同理,如果在后者中也沒有找到需要的引用就會繼續查找它的[[Prototype]],以此類推。這一系列對象的鏈接稱為“原型鏈”。
創建關聯
var foo = {
something: function () {
console.log("Tell me something good...");
}
};
var bar = Object.create(foo);
bar.something(); // Tell me something good...
Object.create(..)會創建一個新對象(bar)并把它關聯到我們指定的對象(foo),這樣我們就可以充分發揮[[Prototype]]機制的威力(委托)并且避免不必要的麻煩(比如使用new的構造函數調用會生成.prototype和.constructor引用)。
Object.create(null)會創建一個擁有空(或者說null)[[Prototype]]鏈接的對象,這個對象無法進行委托。由于這個對象沒有原型鏈,所以instanceof操作符無法進行判斷,因此總是會返回false。這些特殊的空[[Prototype]]對象通常被稱作“字典”,它們完全不會受到原型鏈的干擾,因此非常適合用來存儲數據。
Object.create()的polyfill代碼
Object.create(..)是在ES5中新增的函數,所以在ES5之前的環境中(比如就IE)如果要支持這個功能的話就需要使用一段簡單的polyfill代碼,它部分實現了Object.create(..)的功能:
if (!Object.create) {
Object.create = function (o) {
function F() { }
F.prototype = o;
return new F();
};
}
這段代碼polyfill代碼使用了一個一次性函數F,我們通過改寫它的.prototype屬性使其指向想要關聯的對象,然后再使用new F()來構造一個新對象進行關聯。
關聯關系是備用
var anotherObject = {
cool: function () {
console.log("cool!");
}
};
var myObject = Object.create(anotherObject);
myObject.cool(); // "cool!"
由于存在[[Prototype]]機制,這段代碼可以正常工作。但是如果你這樣寫只是為了讓myObject在無法處理屬性或者方法時可以使用備用的anotherObject,那么你的軟件就會變得有點“神奇”,而且很難理解和維護。
這并不是說任何情況下都不應該選擇備用這種設計模式,但是這在JavaScript中并不是很常見。所以如果你使用的是這種模式,那或許應當退后一步并重新思考一下這種模式是否合適。
當你給開發者設計軟件時,假設要調用myObject.cool(),如果myObject中不存在cool()時這條語句也可以正常工作的話,那你的API設計就會變得很“神奇”,對于未來維護你軟件的開發者來說這可能不太好理解。
但是你可以讓你的API設計不那么“神奇”,同時仍然能發揮[[Prototype]]關聯的威力:
var anotherObject = {
cool: function () {
console.log("cool!");
}
};
var myObject = Object.create(anotherObject);
myObject.doCool = function () {
this.cool(); // 內部委托!
};
myObject.doCool(); // "cool!"
這里我們調用的myObject.doCool()是實際存在于myObject中的,這可以讓我們的API設計更加清晰。從內部來說,我們的實現遵循的是委托設計模式,通過[[Prototype]]委托到anotherObject.cool()。