iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 25
3
Modern Web

重新認識 JavaScript系列 第 25

重新認識 JavaScript: Day 25 原型與繼承

本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。

購書連結 https://www.tenlong.com.tw/products/9789864344130

讓我們再次重新認識 JavaScript!


在上篇文章當中,我們介紹了 JavaScript 物件的繼承關係。
由於 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('Kuro');

p.sayHello();       // "Hi, I'm Kuro"

當我們透過 new 建構一個 Person 的實體時,以上面範例來說就是物件 p,這個物件 p 的原型物件會自動指向建構式的 prototype 屬性,也就是 Person.prototype

https://ithelp.ithome.com.tw/upload/images/20171228/20065504pGsl39ooQK.png

像上面這張圖,就是我們在上一篇曾介紹過的「原型鏈」的概念。

而透過「原型」來新增方法 (method),其實是非常實用的概念,而且是在原型新增後馬上就可以用:

var Person = function(name){
  this.name = name;
};

var p = new Person('Kuro');

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;
  };
}

參考來源: MDN: Array.prototype.find()


__proto__prototype 的關係?

介紹原型的時候我們曾經提過,在 JavaScript 每一個物件都會有它的原型物件 [[prototype]]

在過去,雖然 JavaScript 沒有提供標準方法讓我們直接對原型物件 [[prototype]] 來進行存取,不過幸運的是,大多數的瀏覽器 (精準一點說,大多數的 JavaScript 引擎) 都有提供一種叫做 __proto__ 的特殊屬性,來讓我們取得某個物件的原型物件。 要注意的是,並非所有 JavaScript 的執行環境都支援 __proto__,而瀏覽器之間對 __proto__ 的支援也並非完全相容。 [註1]

好消息是,自從 ES5 開始,如果我們想要取得某個物件的原型物件時,就可以透過 Object.getPrototypeOf( ) 這個標準方法:

var Person = function(name){
  this.name = name;
};

var p = new Person('Kuro');

// 在 Person.prototype 新增 sayHello 方法
Person.prototype.sayHello = function(){
  return "Hi, I'm " + this.name;
}

// 所以 p 也可以呼叫 sayHello 方法
console.log( p.sayHello() );      // "Hi, I'm Kuro"


console.log(Object.getPrototypeOf( p ) === Person.prototype);         // true
console.log(Object.getPrototypeOf( p ) === Function.prototype);       // false
console.log(Object.getPrototypeOf( Person ) === Function.prototype);  // true

console.log( p.__proto__ === Person.prototype );          // true
console.log( p.__proto__ === Function.prototype );        // false
console.log( Person.__proto__ === Function.prototype );   // true

所以簡單來說,不管是 __proto__ 這個特殊屬性或者是 Object.getPrototypeOf( ) 其實都是取得某個物件的原型物件 [[prototype]] 的方式。


所以,現在我們知道了 __proto__ 其實是順著原型鏈向上取得原型物件的特殊屬性,那麼 prototype 呢?

前面說過,「每一個函式被建立之後,都會自動產生一個 prototype 的屬性」,但這並 "不" 代表這個 prototype 屬性就是這個函式的原型物件,而是透過 new 這個函式「建構」出來的物件會有個 [[prototype]] 的隱藏屬性,會指向建構函式的 prototype 屬性。

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


前面說的都是「建構式」與「原型」的關係,那麼如果我們要透過「物件」來達到原型繼承的話可以怎麼做?

第一個是透過昨天介紹的 Object.setPrototypeOf()

像這樣,將 cutman 指定為 rockman 的原型物件:

Object.setPrototypeOf(rockman, cutman);

這裡就不重複說明,可以去看昨天的文章。

第二種,我們可以透過 Object.create()

// Person 物件
var Person = {
  name: 'Default_Name',
  sayHello: function(){
    return "Hi, I'm " + this.name;
  }
};

// 透過 Object.create() 將 Person 作為原型物件來建立一個新的物件
var p = Object.create(Person);

p.sayHello();   // "Hi, I'm Default_Name"

p.name = 'Kuro';
p.sayHello();   // "Hi, I'm Kuro"

像這樣,我們可以先建立一個物件作為「原型」,然後透過 Object.create() 來產生一個新的物件,此時新物件的 [[prototype]] 就會是我們所指定的那個原型物件。

Object.create() 實作的原理簡單來說就像這樣:

Object.create = function (proto){
  function F() {}
  F.prototype = proto;
  return new F();
}

當我們把原型物件作為參數傳入 protoObject.create() 會回傳一個 new F(),也就是透過一個封裝過的建構式建構出來的物件,並把 prototype 指向作為參數的 proto


所以看到這裡,現在我們可以簡單歸納 __proto__prototype 的關係:

以 JavaScript 的內建物件 (build-in object) 來說,像是 ArrayFunction ...等,它們的 prototype 屬性也是一個物件,實際上是繼承自 Object.prototype 而來。

所以我們做個簡單測試:

console.log( Object.getPrototypeOf(Function.prototype
) === Object.prototype );   // true

// 或是透過 __proto__
console.log( Function.prototype.__proto__ === Object.prototype );   // true

像上面這樣,可以看出在原型鏈內下層物件的 __proto__ 屬性或是透過 Object.getPrototypeOf() 取得的物件,實際上會指向上層物件的 prototype 屬性,就可以看出它們之間的繼承關係。

我們花了兩天文章在解釋的東西,大致上可以用這張圖來表示 (原型鏈內物件彼此之間的關係)

https://ithelp.ithome.com.tw/upload/images/20171228/20065504Yf8N277vXl.jpg


最後跟大家分享一則有趣的 推特

https://ithelp.ithome.com.tw/upload/images/20171228/20065504rr1etS0G5J.jpg

相信在理解了 JavaScript 原型鏈的繼承原理之後,看了這則推特應該是哈哈大笑而不是大罵花惹發了吧 XD

如同我曾在 Day3 結尾說的,

雖然這樣都是合法的,但為了身心健康,請不要惡搞你的程式碼

當然同事的也不要! XD


  • [註1]:為了支援向下相容性,ES6 已經將 __proto__ 屬性在語言規範中視作一個標準屬性,但考慮到過去瀏覽器的實作不一,仍建議透過 Object.getPrototypeOf() 來取得原型物件。

上一篇
重新認識 JavaScript: Day 24 物件與原型鏈
下一篇
重新認識 JavaScript: Day 26 同步與非同步
系列文
重新認識 JavaScript37

1 則留言

0
ShawnGood
iT邦新手 5 級 ‧ 2019-02-23 14:37:40

第1張圖p右邊的prototype是誤打嗎?實測後p.__proto__才會有值

我要留言

立即登入留言