JavaScript
Interview Preparation
本站筆記已同步更新到我的個人網站囉! 歡迎參觀與閱讀,體驗不同的視覺感受!
在MDN文件中提到,JavaScript並非一個以class為基礎(class-based)的語言(例如Java、C++),儘管在JavaScript中有class
這個關鍵字,但那只是為了開發者撰寫更直觀易懂的語法糖;事實上,JavaScript是以原型為基礎(prototype-based)的語言。
繼承(Inheritance)可以說是物件導向程式設計(object-oriented programming, OOP)最重要的原則之一,繼承可以讓子類別(child class/subclass)沿用父類別(parent class/ superclass)的屬性與功能,以MDN文件的例子而言:
以上例子中,定義了一個Professor
類別,而這個類別中有兩個屬性(properties): name
和 teaches
,以及兩種方法(methods):grade()
和introduceSelf()
類別就像是一個模板,可以創造該類型的物件,每個被創造出來的物件稱為該類別的實例(Instance),而創造實例的過程則是透過一種特殊的函式- 建構式(Constructor) 達成。
至於什麼是繼承呢,再來看另一個類別Student
:
在Student
這個類別中,可以發現和Professor
類別擁有相同的屬性name
以及相同的方法 introduceSelf()
,因此我們可以定義一個Person
類別作為這兩個類別的父類別,讓這兩個類別繼承Person
的屬性或方法:
Ref: MDN doc, Inheritance in OOPS: An Idea of Code Reusability
以下這兩段來自MDN文件的描述,快速說明了JS原型繼承的特性:
某些人認為 JavaScript 並非真正的物件導向 (Object-oriented, OO) 語言。 在「典型 OO」中,你必須定義特定的類別物件,才能定義哪些類別所要繼承的類別。JavaScript 則使用不同的系統 —「繼承」的物件並不會一併複製功能過來,而是透過原型鍊連接其所繼承的功能,亦即所謂的原型繼承 (Prototypal inheritance)。
From MDN文件
關於物件導向程式設計,可以參考這篇文章
JavaScript 就只有一個建構子:物件。每個物件都有一個連著其他原型(prototype)的私有屬性(private property)物件。原型物件也有著自己的原型,於是原型物件就這樣鏈結,直到撞見 null 為止:null 在定義裡沒有原型、也是原型鏈(prototype chain)的最後一個鏈結。 幾乎所有 JavaScript 的物件,都是在原型鏈最頂端的物件實例。
From MDN文件
實際看看以下的程式碼:
console.log(milkProto)
這一行程式碼在console中可以看到:
它的原型中包含建構式(constructor)Number()
函式以及各種這個原型建構的實例可以使用的方法(method),[[Prototype]]
則可以觀察到這個Number
的原型是Object
,也就是原型鏈的上一層。
console.log(milkProtoProto)
這一行程式碼在console中則可以看到:
__proto__: (...)
也就是再往原型鏈的上層找不到東西了,所以console.log(milkProtoProtoProto)
印出的是null
說了那麼多,直接用程式碼操作:
extends
和super
先定義一個classDrink
,接著定義一個classCoffee
:
(關於屬性名稱為何要使用底線,請參考這個問題)
classCoffee
的屬性和方法,繼承自classDrink
,可以使用extends
關鍵字進行繼承,所以改寫Coffee
如下:
Points:
extends
關鍵字讓Coffee
可以使用父類別Drink
中的方法。super
關鍵字呼叫父類別中的建構式,並可以取用父類別的屬性與方法。在以上範例中,super(name, cost)
將name
和cost
兩個argument傳入父類別Drink
中的建構式,並執行產生新的Coffee
物件實例。super
關鍵字必須在this
關鍵字之前使用,確保新的物件已經在父類別的建構式中建立,此時this
會指向這個新建立的物件;如果沒有在this
之前呼叫super
,則會出現reference error
,好的做法是在子類別建構式的第一行使用super
關鍵字。origin
是Coffee
中的新屬性,所以在此處的建構式中定義它。class
語法糖中,我們可以直接把共用的方法(method)寫在class裡面;如果是用ES6以前的建構式,寫法相當於:
或者是使用Object.assign
的語法:
另外,在繼承父類別時,class
語法糖使用extends
關鍵字;ES6以前則可以使用call()
函式,getter、setter則可以使用Object.defineProperty()
函式。
Reference: MDN docs - Object prototypes
接著,用子類別Coffee
創造一個新的實例看看:
上述程式碼中,我們創造了一個新的Coffee
實例latte
,因為latte
可以取用父類別Drink
中的gettername
,所以會回傳儲存在this._name
屬性中的值,也就是latte。
接著再執行以下程式碼:
以上程式碼做了什麼呢?
Coffee
繼承了父類別Drink
的_amount
、amount getter、以及amountAdded
函式latte
,Drink
的建構式把_amount
屬性設為0amountAdded
函式,所以新建立的實例latte
可以呼叫這個方法並執行,使儲存在_amount
屬性的值+1this._amount
屬性中的值,也就是1如果我們再定義另一個子類別Tea
,同樣繼承Drink
的屬性和方法:
一樣試試看是否可以調用name getter:
成功調用name getter! 再來看看和Tea
的原型和剛剛建立的Coffee
是否相同:
有時,我們希望class中具有個別實例中不可調用的方法,但可以直接從該class中調用這些方法。這些方法稱為靜態方法(Static Methods)。這些方法可以用static
關鍵字調用。
延續前面的範例:
直接從Drink
調用rating
方法:
繼承類別,也會繼承靜態方法:
但是如果是該類別或繼承的子類別所建立的實例,調用該方法:
則會出現錯誤,因為無法從實例上調用靜態方法。
最後再看一個例子:
Math
是一個JavaScript的內建物件,它擁有多種靜態屬性與方法。比較特別的是,和大多數的全域物件(global object)不同,Math
不是建構式(constructor),也就是說,無法使用new
運算子來建立一個實例(instance),但是可以直接從Math
調用靜態方法,例如調用Math.log()
這個靜態方法:
本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!