前言
說明 JavaScript 中非常重要的概念,也就是繼承(inheritance)、原型(prototype)和原型鍊(prototype chain)
原型鍊 (prototype chain)
JavaScript 是一門物件導向的程式語言,因為沒有 Class,所以它的繼承方法是透過 「原型」(prototype) 來進行實作。
「原型」繼承的概念是什麼呢? 簡單來說,透過「原型」繼承可以讓本來沒有某個屬性的物件去存取其他物件的屬性。
以洛克人當例子,洛克人打敗了某關卡的頭目之後,就可以擁有那個敵人的武器。
// 洛克人的武器是 buster 飛彈
var rockman = { buster: true };
// 剪刀人的武器是剪刀
var cutman = { cutter: true };
像上面這樣,建立了兩個物件,分別代表洛克人與剪刀人。
然後,我們可以透過 in 來判斷某個屬性是否可以透過這個物件來存取:
// 洛克人的武器是 buster 飛彈
var rockman = { buster: true };
// 剪刀人的武器是剪刀
var cutman = { cutter: true };
很顯然,洛克人目前只有飛彈,並沒有獲得剪刀人的武器 cutter。
那麼,洛克人辛辛苦苦把剪刀人幹掉了之後,這時候就可以取得他的武器。 以 JavaScript 來說,我們就可以透過
Object.setPrototypeOf( ) 將「剪刀人指定為原型」。
在 JavaScript 裡,物件原型是物件的內部屬性,而且無法直接存取 (所以通常會直接被標示為 [[prototype]]),但
我們可以透過 Object.setPrototypeOf( ) 來指定物件之間的原型關係。
Object.setPrototypeOf(rockman, cutman);
像這樣,第一個參數是「繼承者」的物件,第二個則是被當作「原型」的物件。
如果以洛克人的範例來說,可以這樣寫:
// 洛克人的武器是 buster 飛彈
var rockman = { buster: true };
// 剪刀人的武器是剪刀
var cutman = { cutter: true };
console.log( 'buster' in rockman ); // true
console.log( 'cutter' in rockman ); // false
// 指定 cutman 為 rockman 的「原型」
Object.setPrototypeOf(rockman, cutman);
console.log( 'buster' in rockman ); // true
// 透過原型繼承,現在洛克人也可以使用剪刀人的武器了
console.log( 'cutter' in rockman ); // true
不過可惜的是,在原型繼承的規則裡,同一個物件無法指定兩種原型物件。
也就是說,假設我們再新增一個「氣力人」:
// 氣力人的武器是超級手臂
var gutsman = { superArm: true };
// 指定 gutsman 為 rockman 的「原型」
Object.setPrototypeOf(rockman, gutsman);
// 這個時候洛克人也可以使用氣力人的超級手臂
console.log( 'superArm' in rockman ); // true
// 但是剪刀卻不見了,哭哭
console.log( 'cutter' in rockman ); // false
如果我們希望洛克人可以同時使用「剪刀」與「超級手臂」,要怎麼做呢?
幸好在原型繼承之中,有個觀念叫「原型鏈」(Prototype Chain)。
當我們從某個物件要試著去存取「不存在」的屬性時,那麼 JavaScript 就會往它的 [[prototype]] 原型物件去尋找。
所以說,既然洛克人只能繼承剪刀人的武器,那麼我可不可以順勢讓剪刀人去繼承氣力人的超級手臂呢?
// 洛克人的武器是 buster 飛彈
var rockman = { buster: true };
// 剪刀人的武器是剪刀
var cutman = { cutter: true };
// 氣力人的武器是超級手臂
var gutsman = { superArm: true };
// 指定 cutman 為 rockman 的「原型」
Object.setPrototypeOf(rockman, cutman);
// 指定 gutsman 為 cutman 的「原型」
Object.setPrototypeOf(cutman, gutsman);
// 這樣洛克人就可以順著「原型鏈」取得各種武器了!
console.log( 'buster' in rockman ); // true
console.log( 'cutter' in rockman ); // true
console.log( 'superArm' in rockman ); // true
原型 (prototype)
JavaScript 沒有內建 class 的概念,而是透過「原型」的關係,使物件得以繼承自另外一個物件,這個被繼承的物件我們就稱
之為「原型」 (prototype)。當函式被建立的時候,都會有個原型物件 prototype。 透過擴充這個 prototype 的物件,就能讓每
一個透過這個函式建構的物件都擁有這個「屬性」或「方法」:
var Person = function(name){
this.name = name;
};
// 在 Person.prototype 新增 sayHello 方法
Person.prototype.sayHello = function(){
return "Hi, I'm " + this.name;
}
var p = new Person('Bob');
p.sayHello(); // "Hi, I'm Bob"
當我們透過 new 建構一個 Person 的實體時,以上面範例來說就是物件 p,這個物件 p 的原型物件會自動指向建構式的
prototype 屬性,也就是 Person.prototype。透過「原型」來新增方法 (method),其實是非常實用的概念,而且是在原型新增
後馬上就可以用:
var Person = function(name){
this.name = name;
};
var p = new Person('Bob');
p.sayHelloWorld(); // TypeError: p.sayHelloWorld is not a function
Person.prototype.sayHelloWorld = function(){
return "Hello, World!";
}
p.sayHelloWorld(); // "Hello, World!"
像這樣,物件 p 與他的原型鏈上的所有物件原本都沒有 sayHelloWorld( ) 方法,
但我們透過 Person.prototype.sayHelloWorld 新增了對應的方法後,我們無需重新建置物件 p,馬上就可以透過
p.sayHelloWorld( ) 來呼叫。
這種手法,也是很多「Polyfill」用來增強擴充那些舊版本瀏覽器不支援的語法。 如 Array.prototype.find( ) 在 ES6 以前是不
存在的,但我們可以透過檢查 Array.prototype.find 是否存在。
如果不存在就可以對 Array.prototype 新增 find 方法,然後就可以直接使用。
if (!Array.prototype.find) {
Array.prototype.find = function(predicate) {
if (this === null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var value;
for (var i = 0; i < length; i++) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return value;
}
}
return undefined;
};
}
proto 與 prototype 的關係
在 JavaScript 每一個物件都會有它的原型物件 [[prototype]]
在過去,雖然 JavaScript 沒有提供標準方法讓我們直接對原型物件 [[prototype]] 來進行存取,不過大多數的瀏覽器 (精準一點
說,大多數的 JavaScript 引擎) 都有提供一種叫做 __proto__ 的特殊屬性,來讓我們取得某個物件的原型物件。
自從 ES5 開
始,如果我們想要取得某個物件的原型物件時,也可以透過 Object.getPrototypeOf( ) 這個標準方法。
console.log( Object.getPrototypeOf(Function.prototype
) === Object.prototype ) // true
// 或是透過 __proto__
console.log( Function.prototype.__proto__ === Object.prototype ) // true
簡單來說,不管是 __proto__ 這個特殊屬性或者是 Object.getPrototypeOf( ) 其實都是取得某個物件的原型物件
[[prototype]] 的方式。所以,現在我們知道了 __proto__ 其實是順著原型鏈向上取得原型物件的特殊屬性,那麼 prototype
呢?
前面說過,「每一個函式被建立之後,都會自動產生一個 prototype 的屬性」,但這並 “不” 代表這個 prototype 屬性就是這
個函式的原型物件,而是透過 new 這個函式「建構」出來的物件會有個 [[prototype]] 的隱藏屬性,會指向建構函式的
prototype 屬性。
這也是大家在理解「原型」時最容易搞混的地方。