歡迎來我的部落格閱讀:「JavaScript 原型中的哲學思想」
記得當年初試前端的時候,學習JavaScript過程中,原型問題一直讓我疑惑許久,那時候捧著那本著名的紅皮書,看到有關原型的講解時,總是心存疑慮。
當在JavaScript世界中走過不少旅程之後,再次萌發起研究這部分知識的慾望,翻閱了不少書籍和資料,才搞懂__proto__
和prototype
的概念。
故以作此筆記,日後忘了可以回來看看。如果你看的過程中覺得理解有些困難,把例子在程式碼中跑一跑,親手試一試也許能解決不少疑惑。
一切皆為物件
殊不知,JavaScript的世界中的物件,追根溯源來自於一個 null
「一切皆為物件」,這句著實是一手好營銷,易記,易上口,印象深刻。
萬物初生時,一個null
物件,憑空而生,接著Object
、Function
學著null
的模樣塑造了自己,並且它們彼此之間喜結連理,提供了prototype
和constructor
,一個給子孫提供了基因,一個則製造萬千子子孫孫。
在JavaScript中,null
也是作為一個物件存在,基於它繼承的子子孫孫,當屬物件。乍一看,null
像是上帝,而Object
和Function
猶如JavaScript世界中的亞當與夏娃。
原型指標 __proto__
在JavaScript中,每個物件都擁有一個原型物件,而指向該原型物件的內部指標則是__proto__
,通過它可以從中繼承原型物件的屬性,原型是JavaScript中的基因連結,有了這個,才能知道這個物件的祖祖輩輩。從物件中的__proto__
可以訪問到他所繼承的原型物件。
var a = new Array();
a.__proto__ === Array.prototype // true複製程式碼
上面程式碼中,建立了一個Array的例項a
,該例項的原型指向了Array.prototype
。Array.prototype
本身也是一個物件,也有繼承的原型:
a.__proto__.__proto__ === Object.prototype // true
// 等同於 Array.prototype.__proto__ === Object.prototype複製程式碼
這就說了明瞭,Array本身也是繼承自Object的,那麼Object的原型指向的是誰呢?
a.__proto__.__proto__.__proto__ === null // true
// 等同於 Object.prototype.__proto__ === null複製程式碼
所以說,JavaScript中的物件,追根溯源都是來自一個null物件。佛曰:萬物皆空,善哉善哉。
除了使用.__proto__
方式訪問物件的原型,還可以通過Object.getPrototypeOf
方法來獲取物件的原型,以及通過Object.setPrototypeOf
方法來重寫物件的原型。
值得注意的是,按照語言標準,__proto__
屬性只有瀏覽器才需要部署,其他環境可以沒有這個屬性,而且前後的兩根下劃線,表示它本質是一個內部屬性,不應該對使用者暴露。因此,應該儘量少用這個屬性,而是用 Object.getPrototypeof
和Object.setPrototypeOf
,進行原型物件的讀寫操作。這裡用__proto__
屬性來描述物件中的原型,是因為這樣來得更加形象,且容易理解。
原型物件 prototype
函式作為JavaScript中的一等公民,它既是函式又是物件,函式的原型指向的是Function.prototype
var Foo = function() {}
Foo.__proto__ === Function.prototype // true複製程式碼
函式例項除了擁有__proto__
屬性之外,還擁有prototype
屬性。通過該函式構造的新的例項物件,其原型指標__proto__
會指向該函式的prototype
屬性。
var a = new Foo();
a.__proto__ === Foo.prototype; // true複製程式碼
而函式的prototype
屬性,本身是一個由Object
構造的例項物件。
Foo.prototype.__proto__ === Object.prototype; // true複製程式碼
prototype
屬性很特殊,它還有一個隱式的constructor
,指向了建構函式本身。
Foo.prototype.constructor === Foo; // true
a.constructor === Foo; // true
a.constructor === Foo.prototype.constructor; // true複製程式碼
PS: a.constructor
屬性並不屬於a
(a.hasOwnProperty("constructor") === false
),而是讀取的a.__proto__.constructor
,所以上圖用虛線表示a.constructor
,方便理解。
原型鏈
概念:
原型鏈作為實現繼承的主要方法,其基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。
每個建構函式都有一個原型物件(prototype
),原型物件都包含一個指向建構函式的指標(constructor
),而例項都包含一個指向原型物件的內部指標(__proto__
)。
那麼,假如我們讓原型物件等於另一個型別的例項,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立。如此層層遞進,就構造了例項與原型的鏈條,這就是原型鏈的基本概念。
意義:“原型鏈”的作用在於,當讀取物件的某個屬性時,JavaScript引擎先尋找物件本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。以此類推,如果直到最頂層的Object.prototype還是找不到,則返回undefine。
親子鑑定
在JavaScript中,也存在鑑定親子之間DNA關係的方法:
- instanceof 運算子返回一個布林值,表示一個物件是否由某個建構函式建立。
- Object.isPrototypeOf() 只要某個物件處在原型鏈上,isProtypeOf都返回true
var Bar = function() {}
var b = new Bar();
b instanceof Bar // true
Bar.prototype.isPrototypeOf(b) // true
Object.prototype.isPrototypeOf(Bar) // true複製程式碼
要注意,例項b
的原型是Bar.prototype
而不是Bar
一張歷史悠久的圖
這是一張描述了Object
、Function
以及一個函式例項Foo
他們之間原型之間聯絡。如果理解了上面的概念,這張圖是不難讀懂。
從上圖中,能看到一個有趣的地方。
Function.prototype.__proto__
指向了Object.prototype
,這說明Function.prototype
是一個Object
例項,那麼應當是先有的Object
再有Function
。- 但是
Object.prototype.constructor.__proto__
又指向了Function.prototype
。這樣看來,沒有Function
,Object
也不能建立例項。
這就產生了一種類「先有雞還是先有蛋」的經典問題,到底是先有的Object
還是先有的Function
呢?
這麼哲學向的問題,留給你思考了。
我只是感慨:越往JavaScript的深處探索,越覺得這一門語言很哲學。
先有雞還是先有蛋?
update on 2017/01/05
時隔半年,偶爾翻開這篇文章。
對於這個問題,又有了新的思考。
願意跟能看到這裡的你來分享一下。
我們可以先把 Object.prototype
和 Function.prototype
這兩個拎出來看,因為他們本身就是一個例項物件。
為方便理解,我們改一下名字,避免和 Object 和 Function 的強關聯,分別叫:Op
和 Fp
那麼就有這樣的原型鏈存在了
我再描述一下上面的原型鏈,先有 null , 再有了 Op , 然後再有了 Fp ,然後以 Fp 為原型的兩個建構函式 (Object, Function) 出現了。
而作為建構函式,需要有個 prototype 屬性用來作為以該建構函式創造的例項的繼承。
所以Object.prototype = Op, Function.prototype = Fp。