ES6 系列之 Babel 是如何編譯 Class 的(下)

前言

在上一篇 《 ES6 系列 Babel 是如何編譯 Class 的(上)》,我們知道了 Babel 是如何編譯 Class 的,這篇我們學習 Babel 是如何用 ES5 實現 Class 的繼承。

ES5 寄生組合式繼承

function Parent (name) {
    this.name = name;
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = Object.create(Parent.prototype);

var child1 = new Child('kevin', '18');

console.log(child1);

原型鏈示意圖爲:

寄生組合式繼承原型鏈示意圖

關於寄生組合式繼承我們在 《JavaScript深入之繼承的多種方式和優缺點》 中介紹過。

引用《JavaScript高級程序設計》中對寄生組合式繼承的誇讚就是:

這種方式的高效率體現它只調用了一次 Parent 構造函數,並且因此避免了在 Parent.prototype 上面創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發人員普遍認爲寄生組合式繼承是引用類型最理想的繼承範式。

ES6 extend

Class 通過 extends 關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。

以上 ES5 的代碼對應到 ES6 就是:

class Parent {
    constructor(name) {
        this.name = name;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name); // 調用父類的 constructor(name)
        this.age = age;
    }
}

var child1 = new Child('kevin', '18');

console.log(child1);

值得注意的是:

super 關鍵字表示父類的構造函數,相當於 ES5 的 Parent.call(this)。

子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯。這是因爲子類沒有自己的 this 對象,而是繼承父類的 this 對象,然後對其進行加工。如果不調用 super 方法,子類就得不到 this 對象。

也正是因爲這個原因,在子類的構造函數中,只有調用 super 之後,纔可以使用 this 關鍵字,否則會報錯。

子類的 __proto__

在 ES6 中,父類的靜態方法,可以被子類繼承。舉個例子:

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod(); // 'hello'

這是因爲 Class 作爲構造函數的語法糖,同時有 prototype 屬性和 __proto__ 屬性,因此同時存在兩條繼承鏈。

(1)子類的 __proto__ 屬性,表示構造函數的繼承,總是指向父類。

(2)子類 prototype 屬性的 __proto__ 屬性,表示方法的繼承,總是指向父類的 prototype 屬性。

class Parent {
}

class Child extends Parent {
}

console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

ES6 的原型鏈示意圖爲:

ES6 class 原型鏈示意圖

我們會發現,相比寄生組合式繼承,ES6 的 class 多了一個 Object.setPrototypeOf(Child, Parent) 的步驟。

繼承目標

extends 關鍵字後面可以跟多種類型的值。

class B extends A {
}

上面代碼的 A,只要是一個有 prototype 屬性的函數,就能被 B 繼承。由於函數都有 prototype 屬性(除了 Function.prototype 函數),因此 A 可以是任意函數。

除了函數之外,A 的值還可以是 null,當 extend null 的時候:

class A extends null {
}

console.log(A.__proto__ === Function.prototype); // true
console.log(A.prototype.__proto__ === undefined); // true

Babel 編譯

那 ES6 的這段代碼:

class Parent {
    constructor(name) {
        this.name = name;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name); // 調用父類的 constructor(name)
        this.age = age;
    }
}

var child1 = new Child('kevin', '18');

console.log(child1);

Babel 又是如何編譯的呢?我們可以在 Babel 官網的 Try it out 中嘗試:

'use strict';

function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function Parent(name) {
    _classCallCheck(this, Parent);

    this.name = name;
};

var Child = function(_Parent) {
    _inherits(Child, _Parent);

    function Child(name, age) {
        _classCallCheck(this, Child);

        // 調用父類的 constructor(name)
        var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

        _this.age = age;
        return _this;
    }

    return Child;
}(Parent);

var child1 = new Child('kevin', '18');

console.log(child1);

我們可以看到 Babel 創建了 _inherits 函數幫助實現繼承,又創建了 _possibleConstructorReturn 函數幫助確定調用父類構造函數的返回值,我們來細緻的看一看代碼。

_inherits

function _inherits(subClass, superClass) {
    // extend 的繼承目標必須是函數或者是 null
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
    }

    // 類似於 ES5 的寄生組合式繼承,使用 Object.create,設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });

    // 設置子類的 __proto__ 屬性指向父類
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

關於 Object.create(),一般我們用的時候會傳入一個參數,其實是支持傳入兩個參數的,第二個參數表示要添加到新創建對象的屬性,注意這裏是給新創建的對象即返回值添加屬性,而不是在新創建對象的原型對象上添加。

舉個例子:

// 創建一個以另一個空對象爲原型,且擁有一個屬性 p 的對象
const o = Object.create({}, { p: { value: 42 } });
console.log(o); // {p: 42}
console.log(o.p); // 42

再完整一點:

const o = Object.create({}, {
    p: {
        value: 42,
        enumerable: false,
        // 該屬性不可寫
        writable: false,
        configurable: true
    }
});
o.p = 24;
console.log(o.p); // 42

那麼對於這段代碼:

subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });

作用就是給 subClass.prototype 添加一個可配置可寫不可枚舉的 constructor 屬性,該屬性值爲 subClass。

_possibleConstructorReturn

函數裏是這樣調用的:

var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

我們簡化爲:

var _this = _possibleConstructorReturn(this, Parent.call(this, name));

_possibleConstructorReturn 的源碼爲:

function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

在這裏我們判斷 Parent.call(this, name) 的返回值的類型,咦?這個值還能有很多類型?

對於這樣一個 class:

class Parent {
    constructor() {
        this.xxx = xxx;
    }
}

Parent.call(this, name) 的值肯定是 undefined。可是如果我們在 constructor 函數中 return 了呢?比如:

class Parent {
    constructor() {
        return {
            name: 'kevin'
        }
    }
}

我們可以返回各種類型的值,甚至是 null:

class Parent {
    constructor() {
        return null
    }
}

我們接着看這個判斷:

call && (typeof call === "object" || typeof call === "function") ? call : self;

注意,這句話的意思並不是判斷 call 是否存在,如果存在,就執行 (typeof call === "object" || typeof call === "function") ? call : self

因爲 && 的運算符優先級高於 ? :,所以這句話的意思應該是:

(call && (typeof call === "object" || typeof call === "function")) ? call : self;

對於 Parent.call(this) 的值,如果是 object 類型或者是 function 類型,就返回 Parent.call(this),如果是 null 或者基本類型的值或者是 undefined,都會返回 self 也就是子類的 this。

這也是爲什麼這個函數被命名爲 _possibleConstructorReturn

總結

var Child = function(_Parent) {
    _inherits(Child, _Parent);

    function Child(name, age) {
        _classCallCheck(this, Child);

        // 調用父類的 constructor(name)
        var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

        _this.age = age;
        return _this;
    }

    return Child;
}(Parent);

最後我們總體看下如何實現繼承:

首先執行 _inherits(Child, Parent),建立 Child 和 Parent 的原型鏈關係,即 Object.setPrototypeOf(Child.prototype, Parent.prototype)Object.setPrototypeOf(Child, Parent)

然後調用 Parent.call(this, name),根據 Parent 構造函數的返回值類型確定子類構造函數 this 的初始值 _this。

最終,根據子類構造函數,修改 _this 的值,然後返回該值。

ES6 系列

ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啓發,歡迎 star,對作者也是一種鼓勵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章