經過原型繼承理解ES6 extends 如何實現繼承

前言

第一次接觸到 ES6 中的 class 和 extends 時,就聽人說這兩個關鍵字不過是語法糖而已。它們的本質仍是 ES3 的構造函數,原型鏈那些東西,沒有什麼新鮮的,只要理解了原型鏈等這些概念天然就明白了。這話說的沒錯,可是這些繼承的實現是不是咱們想的那樣呢,今天讓咱們來用原型鏈解釋下 ES6 extends 如何實現的繼承。函數

結論

這裏先上結論,若是有理解不對的地方,歡迎在留言指出;若是有不理解的地方能夠看完結論後繼續閱讀,若是閱讀完後有難以理解指出也歡迎留言討論。this

  1. extends 的繼承經過兩種方式完成了三類值的繼承
  2. 構造函數設置的屬性經過複製完成繼承
  3. 實例方法經過實例原型之間的原型鏈完成繼承
  4. 構造函數的靜態方法經過構造函數之間的原型鏈完成繼承

屬性經過複製完成繼承

class 實例對象屬性的繼承是經過複製達到繼承效果的,這裏的屬性指的是經過構造函數的 this 定義的屬性。spa

class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
}
const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaozhi')

console.log(personA.hasOwnProperty('maxage'));
console.log(programmerB.hasOwnProperty('maxage'));複製代碼

以上代碼的打印結果都是true,這個結果就證實了對象的 extends 繼承的屬性是經過複製繼承的,而不是經過原型鏈完成的屬性繼承。prototype

咱們將以上代碼中獲得的兩個實例對象打印出來,能夠獲得以下圖結果3d

根據打印結果能夠更直觀的看到兩個實例對象上均有 maxage 屬性。原始類型的值的複製好理解,直接拷貝值就好,那麼引用類型的複製是深拷貝,仍是淺拷貝,或者說僅僅是對象引用的拷貝呢?code

構造函數對象值的繼承,比想象中要複雜一點,根據代碼實踐(暫未查看標準),得出結論,引用類型的繼承主要分爲兩種狀況:cdn

  1. 字面量定義的對象屬性是深拷貝
  2. 變量賦值對象屬性是引用複製

字面量定義的對象屬性是深拷貝

這裏的字面量定義的對象屬性指的是,指的是直接在構造函數中經過 {} 的形式定義的對象直接賦值給 this 的某個屬性。代碼形如對象

class A {
    constructor() {
        this.obj = {
            name: 'obj',
            secondObj: {
                name: 'secondObj'
            }
        }
    }
}複製代碼

示例代碼中,obj 屬性是直接經過 {} 定義的一個對象。blog

class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name;
        this.obj = {
            name: 'obj',
            secondObj: {
                name: 'secondObj'
            }
        };
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj)
console.log(personA.obj.secondObj === programmerB.obj.secondObj)複製代碼

上述代碼的運行結果以下繼承

Person 的實例對象上定義了一個 obj 屬性,該屬性被 Programmer 的實例對象繼承,經過對比這兩個屬性的值,咱們知道他們並不相等,這首先排除了是引用複製的可能(若是是引用複製,這裏兩個屬性應該指向同一個對象,也就是其存儲的內存地址應該是致的,可是這裏獲得的結果應該是等式不成立)。經過實例對象屬性 obj 中的 secondObj 屬性的比較,排除了這是淺拷貝,由此咱們能夠得出在代碼示例的場景中引用類型的繼承是經過深拷貝完成的。

變量賦值對象屬性是引用複製

按理來講咱們得出上一小節的結論應該基本就能夠肯定 extends 繼承是如何處理引用類型的值的繼承了,可是事實是到這裏並無結束。

考慮以下代碼,這段代碼和上一小節的代碼區別不大,有變化的地方是,這是在外部定義了一個變量,變量的值是對象,而後將變量賦值給了了 obj 屬性。

let obj = {
    name: 'obj'
}
class Person {
    constructor(name) {
        this.maxage = 100;
        this.name = name;
        this.obj = obj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);複製代碼

運行結果以下

從代碼運行結果中不難看出,這裏出現了變化,經過變量賦值定義的對象屬性,是經過引用複製完成繼承的。下面咱們來看看對象變量被定義在構造函數中而後再賦值給對象的屬性是否仍是這樣的結果。

class Person {
    constructor(name) {
        const innerObj = {
            name: 'obj'
        } 
        this.maxage = 100;
        this.name = name;
        this.obj = innerObj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.obj === programmerB.obj);複製代碼

運行結果以下

沒錯當你把變量定義在構造函數中,而後來賦給 this 的屬性的時候,是經過深拷貝來繼承的。神奇不,一樣是變量,只是變量定義的做用域不同,連繼承方式都變了,具體爲何要這麼作,我如今還不太清楚,改日查下標準,有知道的同窗還望評論區不吝賜教。

小結

這節有點長,須要個小結總結下咱們獲得的結論。首先,extends 的構造函數定義的屬性值的繼承是經過複製繼承的。第二點,副職的方式主要分爲如下幾重情形:

  1. 原始類型直接複製值到子類對象
  2. 引用類型的值若是值是直接在構造函數中定義的(包括字面量直接賦值給屬性和在構造函數內定義的變量而後變量賦值給屬性),那麼其會被深拷貝到子類對象上
  3. 在構造函數外定義的變量,其值是引用類型,構造函數中將該變量賦值給對象的某個屬性,該屬性會被經過引用複製的方式拷貝到子類對象上

實例方法的繼承

實例方法的繼承比較好理解,經過原型鏈原型鏈繼承的,只不過這個鏈的形式是一個比較直接的鏈。這條鏈的大概就像下面這個圖

沒錯,圖上那條紅色的線就是 programmerB 這個實例對象繼承 eat 方法的方式,是否是和想的不同。這條鏈仍是比較好理解的,具備繼承關係的構造函數的 prototype 之間有一條原型鏈,而每一個實例對象的原型又是其構造函數的 prototype,這樣一來就產生了圖中紅色線條實例方法的原型鏈。兩個 class 的實例對象之間沒有什麼關係。

若是對上面的圖存在疑問運行下面這段代碼,運行結果會證實圖是沒有問題的。

class Person {
    constructor(name) {
        const innerObj = {
            name: 'obj'
        } 
        this.maxage = 100;
        this.name = name;
        this.obj = innerObj;
    }
    eat() {
        console.log('eat food')
    }
}
class Programmer extends Person {
    constructor(name) {
        super(name);
        this.job = 'coding'
    }
    coding() {
        console.log('coding world')
    }
}

const personA = new Person('xiaoming')
const programmerB = new Programmer('xiaohei')
console.log(personA.__proto__ === Person.prototype);
console.log(programmerB.__proto__ === Programmer.prototype);
console.log(Programmer.prototype.__proto__ === Person.prototype);
console.log(programmerB.__proto__.__proto__ === Person.prototype);複製代碼

靜態方法的繼承

相比實例方法的繼承,靜態方法的繼承要簡單的多,就是一條簡單的原型鏈,具備繼承關係的兩個 class 之間存在一條原型鏈。以下圖這樣

這個關係圖就沒什麼多說了,有疑問的同窗能夠隨便寫段驗證下。

繼承關係圖

這裏假定 class B extends A,那麼關於原型 class 之間的原型繼承可得出以下等式。

B.proto === Ab.proto === B.prototypea.proto === A.prototypeB.prototype.proto === A.prototypeb.proto.proto === A.prototype

用關係圖來表達上面的這些等式會更容易理解

轉載請註明出處!

相關文章
相關標籤/搜索