JavaScript
物件導向
prototype
本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方 這門課程的學習筆記。如有錯誤歡迎指正!
其實我們在上一篇 Closure & Scope Chain 筆記中,就有稍微提到物件導向的相關觀念:
在物件導向的世界,我們以呼叫 function 的方式進行操作,會比較像是在使用物件的形式。例如 myWallet.add() 這句的意思,就像是對 myWallet 這個物件做一些操作。
像這樣透過塑造物件,就可以不需一直 Call function,也會使程式更模組化一些,並且能夠隱藏內部資訊。
根據 MDN 說明,其實物件導向(Object-oriented)的基本概念就是:「採用物件(Objects)來模塑真實的實物世界」。
也就是說,我們可以在程式中透過 Objects(物件)來塑造其模型,而物件能夠存放其相關資料與程式碼,並且能使用方式(Method)來進行存取。
舉個簡單的例子,以一隻狗來說,可能會有許多基本資訊,像是毛色、身高、名字、性別等,透過這些抽象概念或特質,我們能夠建立一個「Dog」範本,作為代表狗的物件類別(class)。
接著我們可以從這個類別建立物件實體(Object instance),也就是該透過執行類別的「建構子函式(Constructor Function)」建立物件,而這段過程就稱為實體化(Instantiation)。
前面我們提到,JavaScript 是使用類別(class)中的建構子函式(Constructor Function)來定義物件與功能。
但實際上,JavaScript 程式本身並沒有 class 可以使用,是到了 ES6 以後才出現。簡單來說,class 其實是 ES6 提供的語法糖,底層機制仍然是 JavaScript 的原型(prototype)。
接著同樣以建立 Dog 物件類別為例:
new
指令來實體化,建立出物件實體(Object instance)constructor
用來定義物件,也就是在建立時會呼叫 constructor
來執行內部的程式碼,目的是初始化 instanceconstructor
接收,可設定 Dog 的屬性 name
a.sayHello();
:代表 a 透過 sayHello 呼叫 this,所以這個 this 是 a此外,藉由 new 指令建立出 a 和 b 兩個物件,這兩個物件是完全獨立的物件,且都事先具備 Dog 這個 class 的所有屬性。
這其實和物件導向的三大特性有關,這部分我們後面會再提到:
在建立物件實體(Object instance)之後,接著需透過原型(Prototype)機制來相互繼承功能。
前面有提到,class 其實是 ES6 提供的語法糖,底層機制仍然是 JavaScript 的原型(prototype)。因此,即使是在不使用 class 的情況下,我們一樣可以透過 function 和 prototype 來做到 class(類別)的功能。
若將前面的範例改成 ES5 寫法,也就是替換掉 constructor 和 new,如下方程式碼:
會發現 a.sayHello 和 b.sayHello 兩個同樣的 function 卻不是一樣的東西,代表程式在背後其實儲存了兩次。原因在於 class 中是不同的 instance,造成 this 指向不同。
但這其實會產生一個問題,如果今天有一萬隻狗,就會儲存一萬次 function,明明都是在處理同樣的事情,應該改成使用同一個 function 去跑不同的 instance 即可。
.prototype
連結 function在 JavaScript 機制中,有個 .prototype
語法能夠連結 function,如下方程式碼:
Dog.prototype.sayHello
將建構 function 新增一個 sayHello 的屬性兩個 function 就會是相同的,因為 a 和 b 都是 prototype 上面的 function。如此一來,我們就可以使用同一個 function 去跑不同的 instance,透過這個方式實作 JavaScript 的物件導向。
__proto__
& prototype
所以 prototype 是什麼?在 ECMA5.1 標準中,定義原型(prototype)是為其他對象提供共享屬性的對象:
object that provides shared properties for other objects
也就是說,原型(prototype)的本質,就是能夠分享自己的屬性給其他物件使用,而所有的物件都有原型(prototype)屬性。
接著舉一個簡單的物件為例:
可以看到 obj 物件的原型物件,具有的 __proto__
、constructor
等特性,這些都是由原型物件分享給 obj 物件使用的特性:
接著來說明 prototype
和 __proto__
之間的差異:
prototype
(顯性原型)__proto__
(隱性原型)__proto__
屬性會指向該原型物件的 prototype
__proto__
往下一層查找參考資料:js中__proto__和prototype的區別和關係?
接著繼續沿用剛才的 Dog 範例:
會發現 a.__proto__
和 b.__proto__
其實就代表 Dog.prototype,會得到相同結果:
也映證我們前面所說,class 底層其實還是 prototype(原型)這件事,只是寫法不同。
而當我們執行 a.sayHello()
的時候,程式會沿著原型鏈尋找 sayHello 屬性,參考過程如下:
像這樣透過 __proto__
不斷串起來的鏈,就稱作原型鏈(Prototype Chain),其實概念類似於之前提過的 Scope Chain。透過這條原型鏈,就可以引用或繼承自己物件沒有的屬性。
a.hasOwnProperty('sayHello')
來判斷 sayHello 是存在於 instance 還是該原型鏈中在理解 new 在背後做了什麼是之前,先來看這個例子:
會發現 this 其實是非常大的值,裡面包含許多東西:
.call()
:另一種呼叫 function 的方式此外,function 還有一種呼叫方式叫做 .call()
,如果我們 call 來呼叫 test,並在括號內帶入值,將會改變 function 結果:
也就是說,若使用 .call()
呼叫 function,會將傳入的東西設定為 this 的值。
接著我們可以透過「.call()
第一個參數代表 this 指向」這個概念,試著自己建立一個 new 的模型:
透過 function 實作 new 方法,上述的程式碼步驟如下:
.call()
帶入空物件,並設定參數:Dog.call(obj, name)
,第一個參數代表 this 指向 obj,第二個參數代表要帶入的值.__proto__
等同於 Dog 的 .prototype
即可完成關聯當其他類別需要用到共同屬性時,不需再重新建立 class 的各種屬性,可以利用繼承的方法,來直接存取父層的屬性。
例如狗本身有名字有動作,有自己的屬性與方法。而黑狗也屬於一種狗,黑狗也有同樣的屬性與方法,只是會有些微差異,這種情況我們可以透過繼承,也就是讓黑狗 class 直接使用狗 class,就不需再另外建立。
如下方的範例:
如果想要在子層的 class 更改 constructor 屬性時,必須先呼叫 super()
,並把父層 class constructor 需要的參數傳進去。否則就只會初始化子層的 constructor,不會有繼承的作用。
透過 super()
來呼叫父層 class 的 constructor,才能連同父層的 constructor 一併初始化,並接收初始化的值:
透過子層繼承父層類別可延伸多種型態,也就是同樣的方法名稱會有多種行為,使用上也增加彈性,這其實就是物件導向的特性之一:多型(Polymorphism)。
在學習 JavsScript 之前,一直以為物件導向和 this 是能夠畫上等號的(三個的那種)。直到實際學到物件導向以後,才瞭解到物件導向中有許多觀念,其實和在之前學到的 Hoisting、Closure 有很大的關聯。此外,物件導向其實應用在許多現代的程式語言,以物件導向的方式進行開發。
物件導向程式的寫法,基本上可分為三部分:
class Dog
dog.name
存取屬性,使用 dog.sayHello()
存取方法之所以需要物件導向,最重要的目的就是把資料(屬性)與函式(方法)結合在一起,定義出物件模型,這麼做有幾個優點:
而這些概念,其實也就是先前談到有關物件導向的三大特性,並且三者具有次序性,沒有封裝就不可能有繼承、沒有繼承就不可能有多型:
藉由瞭解什麼是物件導向,為什麼需要物件導向以後,對整體架構似乎又更加清楚一些。過程中也查了許多資料,在碰到新的名詞時總會感到慌張,像是 constructor(建構子)、prototype(原型)、instance(實例)等等,其實只要能夠先瞭解定義是什麼,就不難繼續理解整體架構。
最後,在找相關資料的時候,有在這篇網誌中,看到使用泡麵的例子來比喻物件導向,因為還蠻喜歡的也記錄在這裡:
參考資料: