ES6 探祕:Classes

ES6中增長了一些新特性,但從底層的角度來講,只是一些語法糖。可是就我我的來講,若是不瞭解這些語法糖的本質,是用不安心的。那咱們要如何揭開這些語法糖的真實面目呢? express

Babel to the rescue! Babel是一款將ES6代碼轉換爲ES5代碼的編譯器,從而讓咱們能夠無視瀏覽器的支持,直接享受ES6的新特性。同時,咱們也能夠經過研究Babel編譯出的ES5代碼,來揭開ES6的面紗。瀏覽器

ES6 Classes

ES6中的Classes是在Javascript現有的原型繼承的基礎上引入的一種語法糖。Class語法並無引入一種新的繼承模式。它爲對象建立和繼承提供了更清晰,易用的語法。 babel

咱們用class關鍵字來建立一個類,constructor關鍵字定義構造函數,用extends關鍵字來實現繼承,super來實現調用父類方法。app

好,下面是一個ES6 class語法的完整例子:函數

//定義父類View
class View {
  constructor(options) {
    this.model = options.model;
    this.template = options.template;
  }

  render() {
    return _.template(this.template, this.model.toObject());
  }
}
//實例化父類View
var view = new View({
  template: 'Hello, <%= name %>'
});
//定義子類LogView,繼承父類View
class LogView extends View {
  render() {
    var compiled = super.render();
    console.log(compiled);
  }
}

這段簡短的代碼就用到了上述的幾個關鍵詞。class語法的確的簡潔明確,借鑑了主流OO語言的語法,更易於理解。 this

然而我在用這段代碼時,又有些猶豫。這仍是我熟悉的js原型繼承嗎,這真的是同一種繼承模式的一個語法糖嗎? prototype

真相到底是如何呢?咱們就拿babel編譯以後的代碼做爲切入口,來看看ES6 class語法的本質。code

下面是上述ES6代碼用babel編譯以後的結果:對象

'use strict';
var _get = function get(_x, _x2, _x3) {
    var _again = true;
    _function: while (_again) {
        var object = _x,
            property = _x2,
            receiver = _x3;
        desc = parent = getter = undefined;
        _again = false;
        if (object === null) object = Function.prototype;
        var desc = Object.getOwnPropertyDescriptor(object, property);
        if (desc === undefined) {
            var parent = Object.getPrototypeOf(object);
            if (parent === null) {
                return undefined;
            } else {
                _x = parent;
                _x2 = property;
                _x3 = receiver;
                _again = true;
                continue _function;
            }
        } else if ('value' in desc) {
            return desc.value;
        } else {
            var getter = desc.get;
            if (getter === undefined) {
                return undefined;
            }
            return getter.call(receiver);
        }
    }
};

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 _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 View = (function() {
    function View(options) {
        _classCallCheck(this, View);
            this.model = options.model;
        this.template = options.template;
    }
    _createClass(View, [{
        key: 'render',
        value: function render() {
            return _.template(this.template, this.model.toObject());
        }
    }]);
    return View;
})();

var LogView = (function(_View) {
    _inherits(LogView, _View);
    function LogView() {
        _classCallCheck(this, LogView);
        _get(Object.getPrototypeOf(LogView.prototype), 'constructor', this).apply(this, arguments);
    }
    _createClass(LogView, [{
        key: 'render',
        value: function render() {
            var compiled = _get(Object.getPrototypeOf(LogView.prototype), 'render', this).call(this);
            console.log(compiled);
        }
    }]);
    return LogView;  
    
})(View);

這段代碼很長,咱們只關注裏面的函數,能夠獲得它的結構以下:blog

//用於獲得原型鏈上屬性的方法的函數
var _get = function get(_x, _x2, _x3) {
    //······
}

//用於建立對象的函數
var _createClass = (function() {
     //內部函數,定義對象的屬性
    function defineProperties(target, props) {
        //······
    }
    return function(Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();

//用於實現繼承的函數
function _inherits(subClass, superClass) {
        //······
        subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
});

var View = (function() {
    //······
    return View;
})();

var LogView = (function(_View) {
    //······
})(View);

View類的實現

咱們從一個View類的建立開始分析

class View {
  constructor(options) {
    this.model = options.model;
    this.template = options.template;
  }
  render() {
    return _.template(this.template, this.model.toObject());
  }
}

//ES5代碼
var View = (function() {
    function View(options) {
        _classCallCheck(this, View);
            this.model = options.model;
        this.template = options.template;
    }
    _createClass(View, [{
        key: 'render',
        value: function render() {
            return _.template(this.template, this.model.toObject());
        }
    }]);
    return View;
})();

咱們從編譯以後的代碼中能夠看出,View是一個IIFE,裏面是一個同名的函數View,這個函數通過 _createClass()函數的處理以後,被返回了。因此咱們得出的第一點結論就是,ES6中的class實際就是函數。固然這點在各類文檔中已經明確了,因此讓咱們繼續分析。

IIFE中的同名的View實際上就是咱們在ES5的原型繼承中使用的構造函數,因此ES6中的class是對ES5中的構造函數的一種包裝。咱們發現,在class中設定的屬性被放在ES5的構造函數中,而方法則以鍵值對的形式傳入一個_createClass()函數中。那麼這個_createClass()函數又製造了什麼魔法呢?

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;
    };
})();

_createClass也是一個IIFE,有一個內部的函數defineProperties,這個函數遍歷屬性的描述符,進行描述符的默認設置,最後使用Object.defineProperty()方法來寫入對象的屬性。IIFE的renturn部分有兩個分支,一個是針對一個類的原型鏈方法,一個是靜態方法,咱們看到原型鏈方法被寫入構造函數的原型對象裏,而靜態方法則被直接寫入構造函數裏,所以咱們不用實例化對象就能夠直接調用一個類的靜態方法了

js中的函數是對象,Function構造函數的prototype指向Object.prototype,所以能夠寫入屬性

類繼承的實現

OK,到目前咱們已經搞清了ES6的class關鍵字是如何工做的,那麼ES6中的繼承有是如何實現的呢?下面讓咱們看看_inherits()函數。

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;
}

_inherits()函數的關鍵部分即是subClass.prototype = Object.create(···)。經過Object.create()方法來指定新建立對象的原型,由此省去了對父類構造函數的處理,達到了簡單的原型繼承效果。

而後咱們來看看建立LogView類的代碼:

//ES6
class LogView extends View {
  render() {
    var compiled = super.render();
    console.log(compiled);
  }
}
//ES5
var LogView = (function(_View) {
    _inherits(LogView, _View);
    function LogView() {
        _classCallCheck(this, LogView);
        _get(Object.getPrototypeOf(LogView.prototype), 'constructor', this).apply(this, arguments);
    }
    _createClass(LogView, [{
        key: 'render',
        value: function render() {
            var compiled = _get(Object.getPrototypeOf(LogView.prototype), 'render', this).call(this);
            console.log(compiled);
        }
    }]);
    return LogView;
})(View);

LogView類和View類的最大不一樣即是增長了一個_get()函數的調用,咱們仔細看這個_get()函數會發現它接收幾個參數,子類的原型,"constructor"標識符,還有this。再看下面對super.render()的處理,一樣是用_get()函數來處理的。再看_get()函數的源碼,就能夠發現_get()函數的做用即是遍歷對象的原型鏈,找出傳入的標識符對應的屬性,把它用apply綁定在當前上下文上執行。

你們是否是對這種繼承模式似曾相識呢?對了,這就是所謂的「構造函數竊取」。當咱們須要繼承父類的屬性時,在子類的構造函數內部調用父類構造函數,在加上Object.create()大法,而後就能夠繼承父類的全部屬性和方法了。

結語

以上就是筆者對ES6中的class作的一些解讀,依據是babel的編譯結果。今天恰好看到getify大神的一篇博客,將的是ES6中的箭頭函數其實並非一個語法糖,而是一種新的不帶this的函數。他還進一步說了,錯誤的過程獲得正確的結果,會致使不少後續的問題。

而本文的結論是創建在class是一種語法糖的基礎之上的,根據MDN的文檔,咱們應該能夠確認這個結論是可靠的。而ES6中的特性,如箭頭函數,並不都是語法糖,所以在後續的ES6探祕文章中,咱們將用其餘的途徑,來揭示ES6種種神奇魔法的祕密。

相關文章
相關標籤/搜索