如何繼承Date物件?由一道題徹底弄懂JS繼承。

dailc發表於2018-01-15

前言

見解有限,如有描述不當之處,請幫忙及時指出,如有錯誤,會及時修正。

20180201更新:

修改用詞描述,如組合寄生式改成寄生組合式,修改多處筆誤(感謝@Yao Ding的反饋)

----------長文+多圖預警,需要花費一定時間----------

故事是從一次實際需求中開始的。。。

某天,某人向我尋求了一次幫助,要協助寫一個日期工具類,要求:

  • 此類繼承自Date,擁有Date的所有屬性和物件

  • 此類可以自由擴充方法

形象點描述,就是要求可以這樣:

// 假設最終的類是 MyDate,有一個getTest擴充方法
let date = new MyDate();

// 呼叫Date的方法,輸出GMT絕對毫秒數
console.log(date.getTime());
// 呼叫擴充的方法,隨便輸出什麼,譬如helloworld!
console.log(date.getTest());
複製程式碼

於是,隨手用JS中經典的寄生組合式寫了一個繼承,然後,剛準備完美收工,一執行,卻出現了以下的情景:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

但是的心情是這樣的: ?囧

以前也沒有遇到過類似的問題,然後自己嘗試著用其它方法,多次嘗試,均無果(不算暴力混合法的情況),其實回過頭來看,是因為思路新奇,憑空想不到,並不是原理上有多難。。。

於是,藉助強大的搜素引擎,蒐集資料,最後,再自己總結了一番,才有了本文。

----------正文開始前----------

正文開始前,各位看官可以先暫停往下讀,嘗試下,在不借助任何網路資料的情況下,是否能實現上面的需求?(就以10分鐘為限吧)

大綱

  • 先說說如何快速快速尋求解答

    • stackoverflow上早就有答案了!

    • 倘若用的是中文搜尋。

  • 分析問題的關鍵

    • 經典的繼承法有何問題

    • 為什麼無法被繼承?

  • 該如何實現繼承?

    • 暴力混合法

    • ES5黑魔法

    • ES6大法

    • ES6寫法,然後babel打包

  • 幾種繼承的細微區別

  • ES6繼承與ES5繼承的區別

  • 建構函式與例項物件

  • [[Class]]與Internal slot

  • 如何快速判斷是否繼承?

  • 寫在最後的話

先說說如何快速快速尋求解答

遇到不會的問題,肯定第一目標就是如何快速尋求解決方案,答案是:

  • 先去stackoverflow上看看有沒有類似的題。。。

於是,藉助搜尋引擎搜尋了下,第一條就符合條件,點開進去看描述

如何繼承Date物件?由一道題徹底弄懂JS繼承。

stackoverflow上早就有答案了!

先說說結果,再瀏覽一番後,確實找到了解決方案,然後回過頭來一看,驚到了,因為這個問題的提問時間是6 years, 7 months ago。 也就是說,2011年的時候就已經有人提出了。。。

感覺自己落後了一個時代**>_<**。。。

如何繼承Date物件?由一道題徹底弄懂JS繼承。

而且還發現了一個細節,那就是viewed:10,606 times,也就是說至今一共也才一萬多次閱讀而已,考慮到前端行業的從業人數,這個比例驚人的低。 以點見面,看來,遇到這個問題的人並不是很多。

倘若用的是中文搜尋。

用中文搜尋並不丟人(我遇到問題時的本能反應也是去百度)。結果是這樣的:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

嗯,看來英文關鍵字搜尋效果不錯,第一條就是符合要求的。然後又試了試中文搜尋。

如何繼承Date物件?由一道題徹底弄懂JS繼承。
如何繼承Date物件?由一道題徹底弄懂JS繼承。

效果不如人意,搜尋前幾頁,唯一有一條看起來比較相近的(segmentfault上的那條),點進去看

如何繼承Date物件?由一道題徹底弄懂JS繼承。
如何繼承Date物件?由一道題徹底弄懂JS繼承。

怎麼說呢。。。這個問題關注度不高,瀏覽器數較少,而且上面的問題描述和預期的有點區別,仍然是有人回答的。 不過,雖然說問題在一定程度上得到了解決,但是回答者繞過了無法繼承這個問題,有點未竟全功的意思。。。

分析問題的關鍵

藉助stackoverflow上的回答

經典的繼承法有何問題

先看看本文最開始時提到的經典繼承法實現,如下:

/**
 * 經典的js寄生組合式繼承
 */
function MyDate() {
    Date.apply(this, arguments);
    this.abc = 1;
}

function inherits(subClass, superClass) {
    function Inner() {}
    
    Inner.prototype = superClass.prototype;
    subClass.prototype = new Inner();
    subClass.prototype.constructor = subClass;
}

inherits(MyDate, Date);

MyDate.prototype.getTest = function() {
    return this.getTime();
};


let date = new MyDate();

console.log(date.getTest());
複製程式碼

就是這段程式碼⬆,這也是JavaScript高程(紅寶書)中推薦的一種,一直用,從未失手,結果現在馬失前蹄。。。

我們再回顧下它的報錯:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

再列印它的原型看看:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

怎麼看都沒問題,因為按照原型鏈回溯規則,Date的所有原型方法都可以通過MyDate物件的原型鏈往上回溯到。 再仔細看看,發現它的關鍵並不是找不到方法,而是this is not a Date object.

嗯哼,也就是說,關鍵是:由於呼叫的物件不是Date的例項,所以不允許呼叫,就算是自己通過原型繼承的也不行

為什麼無法被繼承?

首先,看看MDN上的解釋,上面有提到,JavaScript的日期物件只能通過JavaScript Date作為建構函式來例項化。

如何繼承Date物件?由一道題徹底弄懂JS繼承。

然後再看看stackoverflow上的回答:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

有提到,v8引擎底層程式碼中有限制,如果呼叫物件的[[Class]]不是Date,則丟擲錯誤。

總的來說,結合這兩點,可以得出一個結論:

要呼叫Date上方法的例項物件必須通過Date構造出來,否則不允許呼叫Date的方法

該如何實現繼承?

雖然原因找到了,但是問題仍然要解決啊,真的就沒辦法了麼?當然不是,事實上還是有不少實現的方法的。

暴力混合法

首先,說說說下暴力的混合法,它是下面這樣子的:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

說到底就是:內部生成一個Date物件,然後此類暴露的方法中,把原有Date中所有的方法都代理一遍,而且嚴格來說,這根本算不上繼承(都沒有原型鏈回溯)。

ES5黑魔法

然後,再看看ES5中如何實現?

// 需要考慮polyfill情況
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
    obj.__proto__ = proto;

    return obj;
};

/**
 * 用了點技巧的繼承,實際上返回的是Date物件
 */
function MyDate() {
    // bind屬於Function.prototype,接收的引數是:object, param1, params2...
    var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

    // 更改原型指向,否則無法呼叫MyDate原型上的方法
    // ES6方案中,這裡就是[[prototype]]這個隱式原型物件,在沒有標準以前就是__proto__
    Object.setPrototypeOf(dateInst, MyDate.prototype);

    dateInst.abc = 1;

    return dateInst;
}

// 原型重新指回Date,否則根本無法算是繼承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);

MyDate.prototype.getTest = function getTest() {
    return this.getTime();
};

let date = new MyDate();

// 正常輸出,譬如1515638988725
console.log(date.getTest());
複製程式碼

一眼看上去不知所措?沒關係,先看下圖來理解:(原型鏈關係一目瞭然)

如何繼承Date物件?由一道題徹底弄懂JS繼承。

可以看到,用的是非常巧妙的一種做法:

  • 正常繼承的情況如下:

    • new MyDate()返回例項物件date是由MyDate構造的

    • 原型鏈回溯是: date(MyDate物件)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype

  • 這種做法的繼承的情況如下:

    • new MyDate()返回例項物件date是由Date構造的

    • 原型鏈回溯是: date(Date物件)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype

可以看出,關鍵點在於:

  • 建構函式裡返回了一個真正的Date物件(由Date構造,所以有這些內部類中的關鍵[[Class]]標誌),所以它有呼叫Date原型上方法的權利

  • 建構函式裡的Date物件的[[ptototype]](對外,瀏覽器中可通過__proto__訪問)指向MyDate.prototype,然後MyDate.prototype再指向Date.prototype。 所以最終的例項物件仍然能進行正常的原型鏈回溯,回溯到原本Date的所有原型方法

  • 這樣通過一個巧妙的欺騙技巧,就實現了完美的Date繼承。不過補充一點,MDN上有提到儘量不要修改物件的[[Prototype]],因為這樣可能會干涉到瀏覽器本身的優化。 如果你關心效能,你就不應該在一個物件中修改它的 [[Prototype]]

如何繼承Date物件?由一道題徹底弄懂JS繼承。

ES6大法

當然,除了上述的ES5實現,ES6中也可以直接繼承(自帶支援繼承Date),而且更為簡單:

class MyDate extends Date {
    constructor() {
        super();
        this.abc = 1;
    }
    getTest() {
        return this.getTime();
    }
}

let date = new MyDate();

// 正常輸出,譬如1515638988725
console.log(date.getTest());
複製程式碼

對比下ES5中的實現,這個真的是簡單的不行,直接使用ES6的Class語法就行了。

而且,也可以正常輸出。

注意:這裡的正常輸出環境是直接用ES6執行,不經過babel打包,打包後實質上是轉化成ES5的,所以效果完全不一樣

ES6寫法,然後Babel打包

雖然說上述ES6大法是可以直接繼承Date的,但是,考慮到實質上大部分的生產環境是:ES6 + Babel

直接這樣用ES6 + Babel是會出問題的

不信的話,可以自行嘗試下,Babel打包成ES5後程式碼大致是這樣的:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

然後當信心滿滿的開始用時,會發現:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

對,又出現了這個問題,也許這時候是這樣的⊙?⊙

因為轉譯後的ES5原始碼中,仍然是通過MyDate來構造, 而MyDate的構造中又無法修改屬於Date內部的[[Class]]之類的私有標誌, 因此構造出的物件仍然不允許呼叫Date方法(呼叫時,被引擎底層程式碼識別為[[Class]]標誌不符合,不允許呼叫,丟擲錯誤)

由此可見,ES6繼承的內部實現和Babel打包編譯出來的實現是有區別的。 (雖說Babel的polyfill一般會按照定義的規範去實現的,但也不要過度迷信)。

幾種繼承的細微區別

雖然上述提到的三種方法都可以達到繼承Date的目的-混合法嚴格說不能算繼承,只不過是另類實現。

於是,將所有能列印的主要資訊都列印出來,分析幾種繼承的區別,大致場景是這樣的:

可以參考:( 請進入除錯模式)dailc.github.io/fe-intervie…

從上往下,1, 2, 3, 4四種繼承實現分別是:(排出了混合法)

  • ES6的Class大法

  • 經典寄生組合式繼承法

  • 本文中的取巧做法,Date構造例項,然後更改__proto__的那種

  • ES6的Class大法,Babel打包後的實現(無法正常呼叫的)

~~~~以下是MyDate們的prototype~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}

~~~~以下是new出的物件~~~~~~~~~
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate2 {abc: 1}
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate {abc: 1}

~~~~以下是new出的物件的Object.prototype.toString.call~~~~~~~~~
[object Date]
[object Object]
[object Date]
[object Object]

~~~~以下是MyDate們的__proto__~~~~~~~~~
ƒ Date() { [native code] }
ƒ () { [native code] }
ƒ () { [native code] }
ƒ Date() { [native code] }

~~~~以下是new出的物件的__proto__~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}

~~~~以下是物件的__proto__與MyDate們的prototype比較~~~~~~~~~
true
true
true
true
複製程式碼

看出,主要差別有幾點:

  1. MyDate們的__proto__指向不一樣

  2. Object.prototype.toString.call的輸出不一樣

  3. 物件本質不一樣,可以正常呼叫的1, 3都是Date構造出的,而其它的則是MyDate構造出的

我們上文中得出的一個結論是:由於呼叫的物件不是由Date構造出的例項,所以不允許呼叫,就算是自己的原型鏈上有Date.prototype也不行

但是這裡有兩個變數:分別是底層構造例項的方法不一樣,以及物件的Object.prototype.toString.call的輸出不一樣。 (另一個MyDate.__proto__可以排除,因為原型鏈回溯肯定與它無關)

萬一它的判斷是根據Object.prototype.toString.call來的呢?那這樣結論不就有誤差了?

於是,根據ES6中的,Symbol.toStringTag,使用黑魔法,動態的修改下它,排除下干擾:

// 分別可以給date2,date3設定
Object.defineProperty(date2, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});
複製程式碼

然後在列印下看看,變成這樣了:

[object Date]
[object Date]
[object Date]
[object Object]
複製程式碼

可以看到,第二個的MyDate2構造出的例項,雖然列印出來是[object Date],但是呼叫Date方法仍然是有錯誤

如何繼承Date物件?由一道題徹底弄懂JS繼承。

此時我們可以更加準確一點的確認:由於呼叫的物件不是由Date構造出的例項,所以不允許呼叫

而且我們可以看到,就算通過黑魔法修改Object.prototype.toString.call,內部的[[Class]]標識位也是無法修改的。 (這塊知識點大概是Object.prototype.toString.call可以輸出內部的[[Class]],但無法改變它,由於不是重點,這裡不贅述)。

ES6繼承與ES5繼承的區別

從上文中的分析可以看到一點:ES6的Class寫法繼承是沒問題的。但是換成ES5寫法就不行了。

所以ES6的繼承大法和ES5肯定是有區別的,那麼究竟是哪裡不同呢?(主要是結合的本文繼承Date來說)

區別:(以SubClassSuperClassinstance為例)

  • ES5中繼承的實質是:(那種經典寄生組合式繼承法)

    • 先由子類(SubClass)構造出例項物件this

    • 然後在子類的建構函式中,將父類(SuperClass)的屬性新增到this上,SuperClass.apply(this, arguments)

    • 子類原型(SubClass.prototype)指向父類原型(SuperClass.prototype

    • 所以instance是子類(SubClass)構造出的(所以沒有父類的[[Class]]關鍵標誌)

    • 所以,instanceSubClassSuperClass的所有例項屬性,以及可以通過原型鏈回溯,獲取SubClassSuperClass原型上的方法

  • ES6中繼承的實質是:

    • 先由父類(SuperClass)構造出例項物件this,這也是為什麼必須先呼叫父類的super()方法(子類沒有自己的this物件,需先由父類構造)

    • 然後在子類的建構函式中,修改this(進行加工),譬如讓它指向子類原型(SubClass.prototype),這一步很關鍵,否則無法找到子類原型(注,子類構造中加工這一步的實際做法是推測出的,從最終效果來推測

    • 然後同樣,子類原型(SubClass.prototype)指向父類原型(SuperClass.prototype

    • 所以instance是父類(SuperClass)構造出的(所以有著父類的[[Class]]關鍵標誌)

    • 所以,instanceSubClassSuperClass的所有例項屬性,以及可以通過原型鏈回溯,獲取SubClassSuperClass原型上的方法

以上⬆就列舉了些重要資訊,其它的如靜態方法的繼承沒有贅述。(靜態方法繼承實質上只需要更改下SubClass.__proto__SuperClass即可)

可以看著這張圖快速理解:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

有沒有發現呢:ES6中的步驟和本文中取巧繼承Date的方法一模一樣,不同的是ES6是語言底層的做法,有它的底層優化之處,而本文中的直接修改__proto__容易影響效能

ES6中在super中構建this的好處?

因為ES6中允許我們繼承內建的類,如Date,Array,Error等。如果this先被建立出來,在傳給Array等系統內建類的建構函式,這些內建類的建構函式是不認這個this的。 所以需要現在super中構建出來,這樣才能有著super中關鍵的[[Class]]標誌,才能被允許呼叫。(否則就算繼承了,也無法呼叫這些內建類的方法)

建構函式與例項物件

看到這裡,不知道是否對上文中頻繁提到的建構函式例項物件有所混淆與困惑呢?這裡稍微描述下:

要弄懂這一點,需要先知道new一個物件到底發生了什麼?先形象點說:

new MyClass()中,都做了些什麼工作

function MyClass() {
    this.abc = 1;
}

MyClass.prototype.print = function() {
    console.log('this.abc:' + this.abc);
};

let instance = new MyClass();
複製程式碼

譬如,上述就是一個標準的例項物件生成,都發生了什麼呢?

步驟簡述如下:(參考MDN,還有部分關於底層的描述略去-如[[Class]]標識位等)

  1. 建構函式內部,建立一個新的物件,它繼承自MyClass.prototypelet instance = Object.create(MyClass.prototype);

  2. 使用指定的引數呼叫建構函式MyClass,並將 this繫結到新建立的物件,MyClass.call(instance);,執行後擁有所有例項屬性

  3. 如果建構函式返回了一個“物件”,那麼這個物件會取代整個new出來的結果。如果建構函式沒有返回物件,那麼new出來的結果為步驟1建立的物件。 (一般情況下建構函式不返回任何值,不過使用者如果想覆蓋這個返回值,可以自己選擇返回一個普通物件來覆蓋。當然,返回陣列也會覆蓋,因為陣列也是物件。)

結合上述的描述,大概可以還原成以下程式碼:(簡單還原,不考慮各種其它邏輯)

let instance = Object.create(MyClass.prototype);
let innerConstructReturn = MyClass.call(instance);
let innerConstructReturnIsObj = typeof innerConstructReturn === 'object' || typeof innerConstructReturn === 'function';

return innerConstructReturnIsObj ? innerConstructReturn : instance;
複製程式碼
  • 注意⚠️:

    • 普通的函式構建,可以簡單的認為就是上述步驟

    • 實際上對於一些內建類(如Date等),並沒有這麼簡單,還有一些自己的隱藏邏輯,譬如[[Class]]標識位等一些重要私有屬性。

      • 譬如可以在MDN上看到,以常規函式呼叫Date(即不加 new 操作符)將會返回一個字串,而不是一個日期物件,如果這樣模擬的話會無效

覺得看起來比較繁瑣?可以看下圖梳理:

如何繼承Date物件?由一道題徹底弄懂JS繼承。

那現在再回頭看看。

什麼是建構函式?

如上述中的MyClass就是一個建構函式,在內部它構造出了instance物件

什麼是例項物件?

instance就是一個例項物件,它是通過new出來的?

例項與構造的關係

有時候淺顯點,可以認為建構函式是xxx就是xxx的例項。即:

let instance = new MyClass();
複製程式碼

此時我們就可以認為instanceMyClass的例項,因為它的建構函式就是它

例項就一定是由對應的建構函式構造出的麼?

不一定,我們那ES5黑魔法來做示例

function MyDate() {
    // bind屬於Function.prototype,接收的引數是:object, param1, params2...
    var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

    // 更改原型指向,否則無法呼叫MyDate原型上的方法
    // ES6方案中,這裡就是[[prototype]]這個隱式原型物件,在沒有標準以前就是__proto__
    Object.setPrototypeOf(dateInst, MyDate.prototype);

    dateInst.abc = 1;

    return dateInst;
}
複製程式碼

我們可以看到instance的最終指向的原型是MyDate.prototype,而MyDate.prototype的建構函式是MyDate, 因此可以認為instanceMyDate的例項。

但是,實際上,instance卻是由Date構造的

我們可以繼續用ES6中的new.target來驗證。

注意⚠️

關於new.targetMDN中的定義是:new.target返回一個指向構造方法或函式的引用

嗯哼,也就是說,返回的是建構函式。

我們可以在相應的構造中測試列印:

class MyDate extends Date {
    constructor() {
        super();
        this.abc = 1;
        console.log('~~~new.target.name:MyDate~~~~');
        console.log(new.target.name);
    }
}

// new操作時的列印結果是:
// ~~~new.target.name:MyDate~~~~
// MyDate
複製程式碼

然後,可以在上面的示例中看到,就算是ES6的Class繼承,MyDate構造中列印new.target也顯示MyDate, 但實際上它是由Date來構造(有著Date關鍵的[[Class]]標誌,因為如果不是Date構造(如沒有標誌)是無法呼叫Date的方法的)。

所以,實際上new.target是無法判斷例項物件到底是由哪一個構造構造的(這裡指的是判斷底層真正的[[Class]]標誌來源的構造)

在MDN上的定義也可以看到,new.target返回的是直接建構函式(new作用的那個),所以請不要將直接建構函式與實際上的構造搞混

再回到結論:例項物件不一定就是由它的原型上的建構函式構造的,有可能建構函式內部有著寄生等邏輯,偷偷的用另一個函式來構造了下, 當然,簡單情況下,我們直接說例項物件由對應建構函式構造也沒錯(不過,在涉及到這種Date之類的分析時,我們還是得明白)。

[[Class]]與Internal slot

這一部分為補充內容。

前文中一直提到一個概念:Date內部的[[Class]]標識

其實,嚴格來說,不能這樣泛而稱之(前文中只是用這個概念是為了降低複雜度,便於理解),它可以分為以下兩部分:

  • 在ES5中,每種內建物件都定義了 [[Class]] 內部屬性的值,[[Class]] 內部屬性的值用於內部區分物件的種類

    • Object.prototype.toString訪問的就是這個[[Class]]

    • 規範中除了通過Object.prototype.toString,沒有提供任何手段使程式訪問此值。

    • 而且Object.prototype.toString輸出無法被修改

  • 而在ES6中,之前的 [[Class]] 不再使用,取而代之的是一系列的internal slot

    • Internal slot 對應於與物件相關聯並由各種ECMAScript規範演算法使用的內部狀態,它們沒有物件屬性,也不能被繼承

    • 根據具體的 Internal slot 規範,這種狀態可以由任何ECMAScript語言型別或特定ECMAScript規範型別值的值組成

    • 通過Object.prototype.toString,仍然可以輸出Internal slot值

    • 簡單點理解(簡化理解),Object.prototype.toString的流程是:如果是基本資料型別(除去Object以外的幾大型別),則返回原本的slot, 如果是Object型別(包括內建物件以及自己寫的物件),則呼叫Symbol.toStringTag

    • Symbol.toStringTag方法的預設實現就是返回物件的Internal slot,這個方法可以被重寫

這兩點是有所差異的,需要區分(不過簡單點可以統一理解為內建物件內部都有一個特殊標識,用來區分對應型別-不符合型別就不給呼叫)。

JS內建物件是這些:

"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"
複製程式碼

ES6新增的一些,這裡未提到:(如Promise物件可以輸出[object Promise]

而前文中提到的:

Object.defineProperty(date, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});
複製程式碼

它的作用是重寫Symbol.toStringTag,擷取date(雖然是內建物件,但是仍然屬於Object)的Object.prototype.toString的輸出,讓這個物件輸出自己修改後的[object Date]

但是,僅僅是做到輸出的時候變成了Date,實際上內部的internal slot值並沒有被改變,因此仍然不被認為是Date

如何快速判斷是否繼承?

其實,在判斷繼承時,沒有那麼多的技巧,就只有關鍵的一點:[[prototype]]__ptoto__)的指向關係

譬如:

console.log(instance instanceof SubClass);
console.log(instance instanceof SuperClass);
複製程式碼

實質上就是:

  • SubClass.prototype是否出現在instance的原型鏈上

  • SuperClass.prototype是否出現在instance的原型鏈上

然後,對照本文中列舉的一些圖,一目瞭然就可以看清關係。有時候,完全沒有必要弄的太複雜。

寫在最後的話

由於繼承的介紹在網上已經多不勝數,因此本文沒有再重複描述,而是由一道Date繼承題引發,展開。(關鍵就是原型鏈)

不知道看到這裡,各位看官是否都已經弄懂了JS中的繼承呢?

另外,遇到問題時,多想一想,有時候你會發現,其實你知道的並不是那麼多,然後再想一想,又會發現其實並沒有這麼複雜。。。

附錄

部落格

初次釋出2018.01.15於我個人部落格上面

www.dailichun.com/2018/01/15/…

參考資料

相關文章