iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
0
Modern Web

JavaScript 忍者的修練--從下忍進階到中忍系列 第 17

Day 17: 物件繼承

程式開發是個集眾人智慧的工作,開發者希望能站在巨人的肩膀,繼承(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 存取到主類別的所有方法。

修複錯誤的 constructor 屬性值

函式在建立時同時會自動生成它的函式原型,並且個別以prototypeconstructor二個屬性記錄對對方的參照。在上面的例子裡,我們卻將函式原型重新指向給新的物件,也就是說更改了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函式所建立的,我們的類別功能總算是完成了!


上一篇
Day 16: Constructor function 的原型機制
下一篇
Day 18: Class
系列文
JavaScript 忍者的修練--從下忍進階到中忍30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言