這是一個我一直沒有搞懂的主題,我也不曉得會分成幾篇,但我會盡力把他搞懂!!!
還記得剛開始學JavaScript時,看到很多文章都在說ES6 的 class 也只是語法糖而已,那時候就在想:
有沒有class有差嗎?我學習到現在都沒有用到過,為什麼很多文章都在強調class呢?
直到我看到了<<JS 原力覺醒>>裡的介紹才知道。
以下內容節錄<<JS 原力覺醒>>的原型章節:
在物件導向語言裡面,類別(class)定義了描述某件事或某個功能的基本概念,就像一件商品或是建築物的設計圖ㄧ樣;而物件(object)則是透過類別裡所描述的的概念實現出來的東西,對比於建築設計圖,就是建築物:
這是一個非常粗淺的描述,這裡提出是為了讓跟我一樣有同樣疑惑的人了解。
那JavaScript是物件導向(OOT)的語言嗎?嚴格來說不完全正確。
更準確的說法應該是:JavaScript是物件原型導向( Object-Prototype Oriented )的語言。
雖然JavaScript支持許多物件導向的概念,如封裝、繼承和多態,但它的實現方式與物件導向語言不同。
JavaScript不是使用類(class)和類繼承,而是使用一種名為「建構式函式(constructor functions)」的函式來定義 object 和它們的特色。
透過建構式函式,你可以更有效率地根據需要創建多個物件。因此,在JavaScript中,每個物件都有一個隱藏的屬性稱為[[ Prototype ]]
,它可以用來共享屬性和方法,或者進行物件之間的複製。
繼承的意思簡單來說就是指:
一個物件可以提取到其他物件中的屬性或方法(One object gets access to the properties and methods of another object.)。
繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance。
先簡單介紹建構式函示,下一篇章會有完整介紹。
建構式函式是一種特殊的function,它是用於創建object 的模板,我們可以使用它來創建具有相似屬性和方法的 object 。通常建構式函式的命名都是以大寫字母為開頭,以區分普通的function。
function Person (){
this.firstName = 'John';
this.lastName = 'Doe';
}
var john = new Person();
console.log(john);
我們看一下輸出:
透過 new
它會幫我們建立一個物件,然後在Person這個function裡面有了屬性名稱跟屬性值。
在JavaScript中,每個 object 都有一個被稱為[[ Prototype ]]
的隱藏屬性,它指向一個 object,而這個object 我們稱之為 prototype object
(原型物件),它裡面包含了一些可以共享的屬性和方法。
因此所有JavaScript的 object都可以從prototype object
中繼承屬性和方法,而prototype object
本身可能又會有有屬於它的prototype object
,而這就是我們所稱的「原型鏈(prototype chain)」。
由於 JavaScript 使用的是 prototypal inheritance,所以必然會包含原型(prototype)的概念,讓我們看一下下面這張圖:
假設我們現在有一個object ,就稱作 obj ,而這個object 包含一個屬性(property),我們稱作 prop1。
如果現在我們想要prop1的屬性值,我只要使用 obj.prop1
的方式就可以直接讀取到 prop1的屬性值。
在前面有提到,JavaScript 裡會有一些預設的屬性和方法,因此所有的object 和 function 都有包含 prototype 這個屬性,現在我們把 prototype 叫做 proto。
這當我們打上 obj.prop2
時,JavaScript 引擎會先在 obj 這個object 的屬性裡去尋找有沒有叫作 prop2 的屬性,如果它找不到,這時候它就會再進一步往該object 的 proto 裡面去尋找。
如果打成
obj.proto.prop2
相信大家會比較了解,但因為prototype chain
的特性我們不需要這樣打。
每一個object 裡面都包含一個 prototype,因此如果我要找prop3的話也是只要打上obj.prop3
就可以了,JavaScript一樣會照上面的步驟執行。
這個從object 本身往 proto 尋找下去的鍊我們就稱作「原型鍊(prototype chain)」。
P.S.它會一直找下去直到某個對象的原型為 null 為止(也就是不再有原型指向)。
我們用實際例子來加強印象:
(這個例子只是為了用來說明 prototype chain 的概念,寫code時千萬不要使用這樣的方式,因為會有效能上的問題!!!)
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}
var john = {
firstname: 'John',
lastname: 'Doe'
}
剛剛的講解讓我們知道所有的物件裡面都會包含原型(prototype)這個物件,在 JavaScript 中這個物件的名稱為 __proto__
。
因此,我們做以下的步驟:
john.__proto__ = person;
console.log(john.getFullName());
console.log(john.firstname);
首先,我們讓john
這個物件就繼承了 person
物件。
如果原本 john 這個物件中找不到這個屬性名稱或方法時,JavaScript 引擎就會到 __proto__
裡面去找,因此第二行的code就會輸出:John Doe
。
那如果他直接在john 這個物件中找到這個屬性名稱或方法時,JavaScript 引擎就不會到 __proto__
裡面去找,因此第三行的code就會輸出:John
。
再往下走,如果這時候我在加一個新的物件:
var jane = {
firstname: 'Jane'
}
jane.__proto__ = person;
console.log(jane.getFullName());
輸出會是:
Jane Default
和你想的一樣吧!
因為在 Jane 這個物件裡只有 firstName 這個屬性,所以當JavaScript 引擎要尋找 getFullName() 這個方法時就會去 __proto__
去找,而這時候的this
是指向Jane(因為是Jane這個object發起調用的),因此他找到了Jane,但Jane這個object沒有lastname,因此他去看了prototype並在那裡找到了lastname,所以我們得到了這個答案!
因此,在 JavaScript 當中,所有的東西(字串、數值、布林值、函式、陣列、物件…)的 prototype 最後都是物件!
JavaScript採用了一種特殊的物件模型,即原型物件模型(Prototype Object Model),與傳統的基於類別的物件導向語言有所不同。
在傳統的物件導向語言中,類別(Class)定義了物件的結構和行為,然後透過類別實例化出具體的物件。在JavaScript中,卻沒有類別的概念,而是使用建構函式(Constructor Function)創建物件,且物件可以直接繼承其他物件,形成原型鏈。
原型鏈(Prototype Chain)是JavaScript物件之間繼承關係的關鍵。每個物件都有一個原型物件(Prototype),可以透過 proto 屬性訪問它。如果在一個物件上訪問一個屬性或方法時找不到,JavaScript引擎會自動沿著原型鏈向上查找,直到找到為止。
這種原型繼承的方式使JavaScript非常靈活,可以動態地擴展和修改物件的屬性和方法,而不需要嚴格的類別定義。這也是為什麼JavaScript被描述為一種"物件原型導向"的語言。
打完了花了好久的時間,主要的時間都是在閱讀其他大神的文章並且修改自己之前文章寫的不清楚或寫錯的地方,還好我有提前開始準備,不然以我的能力怎麼可能每天產出一篇文章。
參考資料:
https://github.com/aszx87410/blog/issues/18
http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html
https://www.jianshu.com/p/a97863b59ef7