0%

JS特訓-DAY40-ES6-團戰關卡-原型、原型鍊 和 繼承的概念

前言

說明 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 屬性。



這也是大家在理解「原型」時最容易搞混的地方。


參考來源