上一篇提到屬性描述符 `[[Get]]` 和 `[[Put]]` 以及提到了訪問描述符 `[[Prototype]]`,看它們的特性就會很容易的讓人想到經典的面向對象風格體系中對類操作要做的事情,但帶一些 introspector 的味道。但我們前幾篇都沒有詳細的提及 js 的原型鏈相關的內容,本篇... ...
上一篇提到屬性描述符 [[Get]]
和 [[Put]]
以及提到了訪問描述符 [[Prototype]]
,看它們的特性就會很容易的讓人想到經典的面向對象風格體系中對類操作要做的事情,但帶一些 introspector 的味道。回想到之前所寫來自用的辣雞私有雲音樂應用中所附帶了一個簡易的類似 jQuery
的簡易常用功能實現,就用到了簡單的 [[Prototype]]
特性。但我們前幾篇都沒有詳細的提及 js 的原型鏈相關的內容,本篇就將討論 js 的 [[Prototype]]
屬性和相關的內容。
註:ES6 的 Proxy 和 class 的概念不在本篇討論範圍內。
[[Prototype]]
JavaScript 中的特殊對象屬性除了 [[Get]]
和 [[Put]]
外,還有一個很重要的特殊內置屬性就是 [[Prototype]]
了。
[[Prototype]]
是一個幾乎所有對象在創建時都會被賦予一個非空值的屬性,還記得在之前提到 new
操作符的行為嗎?其中的行為之一就是把其 [[Prototype]]
關聯指向到對應的內置對象上。通常 [[Prototype]]
所指向的即為創建此對象時所使用的對象了。
來看下麵一個例子
var macat = { a: 1 };
var codingcat = macat; // 和 macat 指向的內容相同
codingcat.b = 2;
console.log(macat.b); // 2
var pineapple = Object.create( macat ); // 新對象,但其 [[Prototype]] 鏈向 macat
pineapple.c = 3; // 新對象的屬性
console.log(macat.c); // undefined
codingcat.d = 4;
console.log(pineapple.d) // 4;
上例中, 變數 codingcat
顯然是指向和 macat
相同的內容,實質完全一致,而 pineapple
則是通過 Object.create()
創建的變數。顯然 pineapple
和 macat
是不同的兩個對象。不過我們會發現我們依然可以通過 pineapple.d
訪問 macat.d
的值,這就是因為在 Object.create()
中,會把 pineapple
的 [[Prototype]]
指向我們的原型對象 macat
了。
那 [[Prototype]]
引用的作用是什麼呢?看上去這是一個確定這種像 fallback 一樣的取值操作應該 fallback 到誰的屬性標記,而準確的說,這種 pineapple.d
形式的屬性引用會觸發 [[Get]]
操作(上篇的內容),而預設的 [[Get]]
則會在對象本身沒有此屬性時會去查找 [[Prototype]]
引用的變數了。這樣的引用成為了鏈狀,故被稱作原型鏈。
當然,這個行為其實我們已經“用過”很多次了,比如 .toString()
、 .valueOf()
、hasOwnProperty()
,我們 Object.create()
等形式構建的新對象顯然並沒有附帶一份這些函數的副本,而是因為普通的 [[Prototype]]
鏈最終都會指向內置的 Object.prototype
,而它提供了這些功能。
屬性設置和屏蔽
不過上例中有個有趣的坑,我們考慮在上例的基礎上做如下操作:
...
pineapple.a++; // 互動式終端會輸出 1
console.log(pineapple.a); // 2
console.log(macat.a); // 1
pineapple.a++
看上去是進行了變數自增的操作,但這一行後,我們發現 pineapple.a
不再等於 macat.a
了,這是因為實際上 pineapple.a
本來並不存在,但可以通過原型鏈找到 macat.a
,而 pineapple.a++
(相當於 pineapple.a = pineapple.a + 1
)最終進行的賦值操作創建了 pineapple.a
,故最終這兩個變數的值自然不再相等。
這個例子來看,如果本身即通過對 pineapple
的屬性(a
)進行訪問操作,那麼不同情況下訪問得到的結果可能是不同的甚至是出人意料的。無意中創建的屬性“阻止”了原型鏈上查找這個屬性的行為,我們稱之為屬性屏蔽。
屬性屏蔽根據變數本身情況的不同會有很多不同的狀態表現,例如原型鏈上層變數的數據訪問屬性標記為只讀的情況,(如果不是嚴格模式下)嘗試進行的賦值操作會被忽略等。
類 (迫真)
我們早已知道 JavaScript 中不存在“類”的概念,而為了能夠“寫著爽”,很多開發者都在想盡辦法在 JavaScript 中模仿其它 OO 語言中“類”的行為。其中很常見的做法類似下麵這樣:
function Person(name) {
console.log("I'm " + name + "!");
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
var chris = new Person("Chris"); // I'm Chris
var sophie = new Person("Sophie"); // I'm Sophie
chris.getName(); // "Chris"
看上去我們的 Person
像極了一個包含 name
成員變數和 getName()
方法的類,並且在其“構造函數”中會輸出 "I'm xxx"。不過在之前的文章中我們已經講過了,並不存在所謂的構造函數,new
只是把 Person()
函數作為構造對象所需調用的函數進行了一次調用而已。不過你可能還會比較奇怪為什麼 .getName()
是可以使用的,既然我們在原型鏈這一章提起這件事,顯然是因為原型鏈,於是回顧之前第二章我們含糊提到的一句話是(new
操作符所執行的操作步驟之一是)“對這個新對象執行 [[Prototype]]
鏈接”,實際上,這裡我們被 new
出來的對象的 [[Prototype]]
被關聯到了 Person.prototype
上,於是當我們嘗試進行屬性訪問的時候,自然就可以訪問到 Person.prototype.getName()
上了。
不過這個過程還是可能會引起一些蛋疼的誤會,比如假設我們在上面例子的基礎上:
...
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // true
Person.prototype = {};
var koishi = new Person("Koishi"); // I'm Koishi
koishi.constructor === Person; // false
koishi.constructor === Object; // true
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // false
由於“構造函數”這種表現形式的理解,我們有時候會認為 變數名.constructor
實際就總是構造調用時指向的函數,甚至 sophie.constructor === Person
返回也是 true
,但實際並不是這樣,這裡返回為真,僅僅是因為 Person.prototype.constructor
預設指向的就是 Person
罷了。於是我們嘗試替換 Person.prototype
之後創建了變數 koishi
,再檢查 koishi.constructor === Person
就不再為真了,在原型鏈的查找過程最終找到了 Object.prototype
,然後 Object.prototype.constructor
其實指向了 Object
。
不過,後面我們接著嘗試檢查了 sophie.constructor
卻發現似乎它並未受到影響,這個就不要往原型鏈方面想了,這裡的原因僅僅是 sophie
的原型鏈指向的是曾經 Person.prototype
所指向的東西上,而我們 Person.prototype = {}
的操作只是讓 Person.prototype
指向了新的東西,舊的東西並沒有改變,所以 sophie
自然看上去“沒有受到影響”了。當然,koishi
這個變數被構造時所被調用的函數仍然是 Person()
,這和 koishi.constructor
或者 Person.prototype.constructor
的指向沒有什麼關係。
對象實例關係
當然我們還有一點需要重新強調的是,[[Prototype]]
和 .prototype
不是一回事,[[Prototype]]
是描述對象實例關係的屬性描述符,而 .prototype
只是 Function
對象的一個屬性而已。new
操作符會把新建的對象的 [[Prototype]]
指向原對象的 .prototype
屬性上,僅此而已。
既然 [[Prototype]]
實際描述了對象之間的實例關係,那麼我們自然就可以想到 instanceof
的實際作用了,其所做的事情就是告訴你在 a instanceof Foo
中, a
的整個原型鏈中是否有指向 Foo.prototype
的對象。
絕大多數瀏覽器支持一個 .__proto__
屬性(實際位於 Object.__proto__
)指向了 [[Prototype]]
,這對於我們調試時希望直接訪問內部的 [[Prototype]]
提供了便利,不過它並不是標準,所以除了調試便利之外還是不要使用它比較好。
最後
於是關於原型鏈相關的簡單討論就到此結束了。和上篇一樣,如果你對這些內容仍然感興趣,不妨去讀一讀《You don’t know JS - this & object prototypes》一書。這是一本開源書,你可以在這裡線上閱讀這本書,或者購買這本書的電子版或實體版。這本書的中文譯本涵蓋在《你所不知道的 JavaScript 上捲》中,你也可以考慮看中文版。
由於近期工作過於繁忙的精力占用緣故,“原來JS是這樣的”系列可能就暫時告一段落了。最後,儘管我會儘可能仔細的檢查文章內容是否有問題,但也不保證這篇文章中一定不會有錯誤,如果您發現文章哪裡有問題,請在下麵留言指正,或通過任何你找得到的方式聯繫我指正。感激不盡~