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

gamebus發表於2021-09-09

前言

在瞭解 Babel 是如何編譯 class 前,我們先看看 ES6 的 class 和 ES5 的建構函式是如何對應的。畢竟,ES6 的 class 可以看作一個語法糖,它的絕大部分功能,ES5 都可以做到,新的 class 寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。

constructor

ES6 中:

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

    sayHello() {
        return 'hello, I am ' + this.name;
    }
}

var kevin = new Person('Kevin');
kevin.sayHello(); // hello, I am Kevin

對應到 ES5 中就是:

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

Person.prototype.sayHello = function () {
    return 'hello, I am ' + this.name;
};

var kevin = new Person('Kevin');
kevin.sayHello(); // hello, I am Kevin

我們可以看到 ES5 的建構函式 Person,對應 ES6 的 Person 類的 constructor 方法。

值得注意的是:類的內部所有定義的方法,都是不可列舉的(non-enumerable)

以上面的例子為例,在 ES6 中:

Object.keys(Person.prototype); // []
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]

然而在 ES5 中:

Object.keys(Person.prototype); // ['sayHello']
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]

例項屬性

以前,我們定義例項屬性,只能寫在類的 constructor 方法裡面。比如:

class Person {
    constructor() {
        this.state = {
            count: 0
        };
    }
}

然而現在有一個提案,對例項屬性和靜態屬性都規定了新的寫法,而且 Babel 已經支援。現在我們可以寫成:

class Person {
    state = {
        count: 0
    };
}

對應到 ES5 都是:

function Person() {
    this.state = {
        count: 0
    };
}

靜態方法

所有在類中定義的方法,都會被例項繼承。如果在一個方法前,加上 static 關鍵字,就表示該方法不會被例項繼承,而是直接透過類來呼叫,這就稱為“靜態方法”。

ES6 中:

class Person {
    static sayHello() {
        return 'hello';
    }
}

Person.sayHello() // 'hello'

var kevin = new Person();
kevin.sayHello(); // TypeError: kevin.sayHello is not a function

對應 ES5:

function Person() {}

Person.sayHello = function() {
    return 'hello';
};

Person.sayHello(); // 'hello'

var kevin = new Person();
kevin.sayHello(); // TypeError: kevin.sayHello is not a function

靜態屬性

靜態屬性指的是 Class 本身的屬性,即 Class.propName,而不是定義在例項物件(this)上的屬性。以前,我們新增靜態屬性只可以這樣:

class Person {}

Person.name = 'kevin';

因為上面提到的提案,現在可以寫成:

class Person {
  static name = 'kevin';
}

對應到 ES5 都是:

function Person() {};

Person.name = 'kevin';

new 呼叫

值得注意的是:類必須使用 new 呼叫,否則會報錯。這是它跟普通建構函式的一個主要區別,後者不用 new 也可以執行。

class Person {}

Person(); // TypeError: Class constructor Foo cannot be invoked without 'new'

getter 和 setter

與 ES5 一樣,在“類”的內部可以使用 get 和 set 關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為。

class Person {
    get name() {
        return 'kevin';
    }
    set name(newName) {
        console.log('new name 為:' + newName)
    }
}

let person = new Person();

person.name = 'daisy';
// new name 為:daisy

console.log(person.name);
// kevin

對應到 ES5 中:

function Person(name) {}

Person.prototype = {
    get name() {
        return 'kevin';
    },
    set name(newName) {
        console.log('new name 為:' + newName)
    }
}

let person = new Person();

person.name = 'daisy';
// new name 為:daisy

console.log(person.name);
// kevin

Babel 編譯

至此,我們已經知道了有關“類”的方法中,ES6 與 ES5 是如何對應的,實際上 Babel 在編譯時並不會直接就轉成這種形式,Babel 會自己生成一些輔助函式,幫助實現 ES6 的特性。

我們可以在 Babel 官網的頁面檢視 ES6 的程式碼編譯成什麼樣子。

編譯(一)

ES6 程式碼為:

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

Babel 編譯為:

"use strict";

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

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

    this.name = name;
};

_classCallCheck 的作用是檢查 Person 是否是透過 new 的方式呼叫,在上面,我們也說過,類必須使用 new 呼叫,否則會報錯。

當我們使用 var person = Person() 的形式呼叫的時候,this 指向 window,所以 instance instanceof Constructor 就會為 false,與 ES6 的要求一致。

編譯(二)

ES6 程式碼為:

class Person {
    // 例項屬性
    foo = 'foo';
    // 靜態屬性
    static bar = 'bar';

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

Babel 編譯為:

'use strict';

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

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

    this.foo = 'foo';

    this.name = name;
};

Person.bar = 'bar';

編譯(三)

ES6 程式碼為:

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

    sayHello() {
        return 'hello, I am ' + this.name;
    }

    static onlySayHello() {
        return 'hello'
    }

    get name() {
        return 'kevin';
    }

    set name(newName) {
        console.log('new name 為:' + newName)
    }
}

對應到 ES5 的程式碼應該是:

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

Person.prototype =  {
    sayHello: function () {
        return 'hello, I am ' + this.name;
    },
    get name() {
        return 'kevin';
    },
    set name(newName) {
        console.log('new name 為:' + newName)
    }
}

Person.onlySayHello = function () {
    return 'hello'
};

Babel 編譯後為:

'use strict';

var _createClass = function() {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
    return function(Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

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

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

        this.name = name;
    }

    _createClass(Person, [{
        key: 'sayHello',
        value: function sayHello() {
            return 'hello, I am ' + this.name;
        }
    }, {
        key: 'name',
        get: function get() {
            return 'kevin';
        },
        set: function set(newName) {
            console.log('new name 為:' + newName);
        }
    }], [{
        key: 'onlySayHello',
        value: function onlySayHello() {
            return 'hello';
        }
    }]);

    return Person;
}();

我們可以看到 Babel 生成了一個 _createClass 輔助函式,該函式傳入三個引數,第一個是建構函式,在這個例子中也就是 Person,第二個是要新增到原型上的函式陣列,第三個是要新增到建構函式本身的函式陣列,也就是所有新增 static 關鍵字的函式。該函式的作用就是將函式陣列中的方法新增到建構函式或者建構函式的原型中,最後返回這個建構函式。

在其中,又生成了一個 defineProperties 輔助函式,使用 Object.defineProperty 方法新增屬性。

預設 enumerable 為 false,configurable 為 true,這個在上面也有強調過,是為了防止 Object.keys() 之類的方法遍歷到。然後透過判斷 value 是否存在,來判斷是否是 getter 和 setter。如果存在 value,就為 descriptor 新增 value 和 writable 屬性,如果不存在,就直接使用 get 和 set 屬性。

寫在後面

至此,我們已經瞭解了 Babel 是如何編譯一個 Class 的,然而,Class 還有一個重要的特性就是繼承,Class 如何繼承,Babel 又該如何編譯,歡迎期待下一篇《 ES6 系列之 Babel 是如何編譯 Class 的(下)》

ES6 系列

ES6 系列目錄地址:

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

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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4687/viewspace-2816592/,如需轉載,請註明出處,否則將追究法律責任。

相關文章