程式開發是個集眾人智慧的工作,開發者希望能站在巨人的肩膀,繼承(inheritance)既有的程式所有功能,在上面開發新功能,不用每次都重新造輪子。在其他程式語言裡有類別(class)的設計讓延伸的子類別繼承主類別的所有功能及特性。JavaScript 沒有「真正」類別的設計(原因下一篇文章會說到),而是使用原型建立類似類別的效果。
前面我們學到,物件原型讓物件可以透過 prototype chain 存取其他物件的屬性,函式原型讓同一個 constructor function 建立的 instance 可以共用相同方法。這些特性使得 constructor function 成為 JavaScript 實現類別的方式。
就像任天堂創造了寶可夢,底下又分成好幾種類型(火系、水系、草系等等),每一種類型雖然有特別的能力,但還是保有共同的寶可夢動物特性。如果今天我們想要建立一個新的 constructor function,希望由它建立的物件能擁有既有 constructor function 的所有功能,然後我們可以增添自定的功能,要如何用原型實現呢?
// 建立主要的 Pokemon constructor function,
// 在原型上加入 fight 方法
function Pokemon() {}
Pokemon.prototype.fight = function() {};
// 另外建立 Pikachu constructor function
function Pikachu() {}
// 把 Pikachu 的函式原型設為 Pokemon 的 instance
Pikachu.prototype = new Pokemon();
// 在這個 instance 上增加 Pikachu 的自有方法
Pikachu.prototype.thunderShock = function() {};
// 新建立的 pikachu 物件同時是 Pikachu 和 Pokemon 的 instance
// 也可以存取 fight 和 thunderShock 方法
const pikachu = new Pikachu();
console.log(pikachu instanceof Pikachu); // true
console.log(pikachu instanceof Pokemon); // true
console.log(typeof pikachu.fight === "function"); // true
console.log(typeof pikachu.thunderShock === "function"); // true
// 增加一個 Ditto 類別
function Ditto() {}
// 函式原型是另一個 Pokemon instance
Ditto.prototype = new Pokemon();
// Ditto 有自己的 transform 方法
Ditto.prototype.transform = function() {};
// 由 Ditto 產生的物件同時是 Pokemon 和 Ditto 的 instance,但不是 Pikachu 的 instance
// 除了自己的和主類別的方法外,無法存取 Pikachu 的方法
const ditto = new Ditto();
console.log(ditto instanceof Pokemon); // true
console.log(ditto instanceof Ditto); // true
console.log(ditto instanceof Pikachu); // false
console.log(typeof ditto.fight === "function"); // true
console.log(typeof ditto.transform === "function"); // true
console.log(ditto.thunderShock); // undefined
具體作法是把既有的 constructor function 的 instance 設為新的 constructor function 的函式原型,如此一來子類別的物件能透過 prototype chain 存取到主類別的所有方法。
函式在建立時同時會自動生成它的函式原型,並且個別以prototype
及constructor
二個屬性記錄對對方的參照。在上面的例子裡,我們卻將函式原型重新指向給新的物件,也就是說更改了prototype
的對象,然而constructor
的對象卻沒有變,這麼一來會造成一些問題。
以邏輯來看,pikachu
instance 是從 Pikachu
constructor function 建立出來的,所以我們預期這樣的檢查是通過的:
console.log(pikachu.constructor === Pikachu);
然而我們卻得到false
的結果!仔細看上面的圖發現,因為重新定義函式原型,pikachu
instance 已經失去和原本的Pikachu
函式原型的關聯,在它的 prototype chain 上其實是Pokemon
instance 和Pokemon
的函式原型。所以當搜尋constructor
屬性時,在 prototype chain 上找到的是在Pokemon
函式原型裡的constructor
屬性,而它指向的對象是Pokemon
constructor function!
constructor
屬性可以用來判斷這個 instance 是從哪個 constructor function 建立的,其他的開發者預期得到的是正確的結果,但是我們卻弄壞它。幸好可以用 Object.defineProperty()
來改寫屬性。
這個方法的內容在這裡就不多說,請參考 MDN 的說明文件。
Object.defineProperty(Pikachu.prototype, "constructor", {
enumerable: false,
value: Pikachu,
writable: true
});
console.log(pikachu.constructor === Pikachu); // true
在Pikachu
的函式原型裡增加了constructor
屬性,重建屬性和原型之間的關聯,再次檢查就會得到正確的值,現在我們可以判斷出pikachu
instance 是由Pikachu
函式所建立的,我們的類別功能總算是完成了!