ES6中的class是如何實現的?(附Babel編譯的ES5代碼詳解)

序言

這篇文章主要講解面試某大廠遇到的一個問題 - ES6中的class語法的實現?前端

ECMAScript 6 實現了class,class是一個語法糖,使得js的編碼更清晰、更人性化、風格更接近面向對象的感受;也使 IDE 、編譯期類型檢查器、代碼風格檢查器等工具更方便地檢測代碼語法,作靜態分析。一樣的,這給沒有類就缺點什麼的軟件開發工程師一個更低的門檻去接觸js。面試

ES6 class 的 ES5 代碼實現

JavaScript語言的傳統方法是經過構造函數定義並生成新對象,這種寫法和傳統的面嚮對象語言差別較大。因此,ES6引入了Class這個概念做爲對象的模板。數組

constructor

效果:ES6建立一個class會默認添加constructor方法,並在new調用時自動調用該方法。bash

ES5:babel

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

Person.prototype.toString = function () {
    return '(' + this.name + ',' + this.age + ')';
}
var p = new Person('Mia', 18);
console.log(p);// Person { name: 'Mia', age: 18 }
複製代碼

ES6:函數

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {
        return '(' + this.name + ',' + this.age + ')';
    }
}
var p = new Person('Mia', 18);
console.log(p);// Person { name: 'Mia', age: 18 }
複製代碼

ES6的classconstructor是構造方法,對應的是ES5中的構造函數Personthis關鍵字則表明實例對象。工具

裏面的class類能夠看作是構造函數的另外一種寫法,由typeof Person === 'function'truePerson === Person.prototype.constructortrue能夠得出,類的數據類型就是函數,類自己指向構造函數。也能夠說class的底層依然是function構造函數ui

類的公共方法都定義在類的prototype屬性上。可使用Object.assign一次向類添加多個方法。this

特別的:class的內部定義的方法都是不可枚舉的(non-enumerable),這一點與ES5的行爲不一致。編碼

ES5:

Object.keys(Person.prototype); // ['toString']
複製代碼

ES6:

Object.keys(Person.prototype); // Person {}
複製代碼

不可枚舉的代碼實現會在後面將ES6代碼用Babel轉碼以後解析。

new調用

效果:class類必須使用new調用,不然會報錯。

ES5:

Person()// undefined
複製代碼

ES6:

Person() // TypeError: Class constructor Person cannot be invoked without 'new'
複製代碼

實例的屬性

效果:實例的屬性是顯式定義在this對象上,不然都是定義在原型上。類的全部實例共享一個原型對象,與ES5行爲一致。

ES5:

function Person() {
    this.grade = {
        count: 0
    };
}
複製代碼

ES6:

class Person {
    constructor() {
        this.grade = {
            count: 0
        };
    }
}
複製代碼

此外還要關注新提案,Babel已經支持實例屬性和靜態屬性新的寫法。

靜態方法

類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接經過類來調用,這就稱爲「靜態方法」。

注意:若是靜態方法包含this關鍵字,指的是類。

ES5:

function Person() { }
Person.toSay = function () {
    return 'I love JavaScript.';
};
Person.toSay(); // I love JavaScript.
複製代碼

ES6:

class Person {
    static toSay() {
        return 'I love JavaScript.';
    }
}
Person.toSay(); // I love JavaScript.
複製代碼

getter 和 setter

ES6提供 getset 關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲,和ES5行爲一致。

ES5:

function Person(name) {}
Person.prototype = {
    get name() {
        return 'mia';
    },
    set name(newName) {
        console.log('new name:' + newName);
    }
}
複製代碼

ES6:

class Person {
    get name() {
        return 'mia';
    }
    set name(newName) {
        console.log('new name:' + newName);
    }
}
複製代碼

其餘class特性

  • 不存在變量提高
  • 不能覆蓋聲明

ES6 class 底層實現原理 - Babel是如何編譯class的

下文主要用babel轉碼器分別對class中幾個主要的方法進行轉碼,分析ES5的實現方式。 將下面的代碼使用babel轉碼器轉換成ES5代碼,按照代碼結構和功能分塊進行講解。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    toString() {
        return '(' + this.name + ',' + this.age + ')';
    }
}
var p = new Person('Mia', 18);
複製代碼

運行模式

"use strict";//class默認開啓嚴格模式
複製代碼

私有函數

JS開發者在變量名或函數名前綴加下劃線,通常表示私有。

前綴加下劃線表示私有僅僅是一個約定俗成的習慣,澄清意圖,並無作其餘處理。因爲ECMAScript草案中並無定義私有變量的方法,因此在此限定之下仍能夠在函數外或做用域外訪問該函數或變量。

_instanceof和_classCallCheck的做用

檢查聲明的class類是否經過new的方式調用,不然會報錯。

function _instanceof(left, right) {
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
        return right[Symbol.hasInstance](left);
    } else {
        return left instanceof right;
    }
}
function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
複製代碼

_createClass和_defineProperties的做用

_createClass函數有三個參數,Constructor是傳入構造函數Person,protoProps 是要添加到原型上的函數數組,staticProps 是要添加到構造函數自己的函數,即靜態方法。這裏的第二個和第三個參數是能夠缺省的,會在_createClass 函數體內判斷。

_createClass 函數的做用是收集公有函數和靜態方法,將方法添加到構造函數或構造函數的原型中,並返回構造函數。

defineProperties 是將方法添加到構造函數或構造函數的原型中的主要邏輯,遍歷函數數組,分別聲明其描述符。若enumerable 沒有被定義爲true,則默認爲fals,設置 configurabletrue。以上兩個布爾值是爲了限制 Object.keys() 之類的方法被遍歷到。若是存在 value,就爲 descriptor 添加 valuewritable 屬性,若是不存在,就直接使用 getset 屬性。

最後,使用 Object.defineProperty 方法爲構造函數添加屬性。

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);
    }
}
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}
複製代碼

class類實現主體代碼

var Person =
    /*#__PURE__*/
    function () {
        function Person(name, age) {
            _classCallCheck(this, Person);

            this.name = name;
            this.age = age;
        }

        _createClass(Person, [{
            key: "toString",
            value: function toString() {
                return '(' + this.name + ',' + this.age + ')';
            }
        }]);

        return Person;
    }();

var p = new Person('Mia', 18);
複製代碼

解析:

  • 不使用new調用時,this指向window,因此instance instanceof Constructorfalse,拋出異常。
  • 經過調用_createClass函數,遍歷函數數組。key爲方法名,如有value說明是有具體的 function 聲明,若無 value 說明使用了get 或 set 方法。
  • Person類是經過聲明一個IIFE實現的,IIFE是當即執行函數,建立即執行。能夠保證變量的做用域限制在函數內,避免命名衝突。

結尾

讀到這相信你們對class的實現有了更深的理解。筆者也是一邊在忙畢業設計,一邊整理了這道阿里前端面試題的解析。但願你們有所收穫。

評論區歡迎對ES6 class實現原理相關問題進行討論。另外,class中的extend也是頗有趣的實現,在下一篇文章會對class實現繼承進行解析。

相關文章
相關標籤/搜索