JavaScript 基礎:Babel 轉譯 class 過程窺探

零、前言

雖然在 JavaScript 中對象無處不在,但這門語言並不使用經典的基於類的繼承方式,而是依賴原型,至少在 ES6 以前是這樣的。當時,假設咱們要定義一個能夠設置 id 與座標的類,咱們會這樣寫:git

// Shape 類
function Shape(id, x, y) {
    this.id = id;
    this.setLocation(x, y);
}

// 設置座標的原型方法
Shape.prototype.setLocation = function(x, y) {
    this.x = x;
    this.y = y;
};

上面是類定義,下面是用於設置座標的原型方法。從 ECMAScript 2015 開始,語法糖 class被引入,開發者能夠經過 class 關鍵字來定義類。咱們能夠直接定義類、在類中寫靜態方法或繼承類等。上例即可改寫爲:github

class Shape {
    constructor(id, x, y) { // 構造函數語法糖
        this.id = id;
        this.setLocation(x, y);
    }

    setLocation(x, y) { // 原型方法
        this.x = x;
        this.y = y;
    }
}

一個更符合「傳統語言」的寫法。語法糖寫法的優點在於當類中充滿各種靜態方法與繼承關係時,class 這種對象模版寫法的簡潔性會更加突出,且不易出錯。但不能否認時至今日,咱們還須要爲某些用戶兼容咱們的 ES6+ 代碼,class 就是 TodoList 上的一項:npm

做爲當下最流行的 JavaScript 編譯器,Babel 替咱們轉譯 ECMAScript 語法,而咱們不用再擔憂如何進行向後兼容。數組

本地安裝 Babel 或者利用 Babel CLI 工具,看看咱們的 Shape 類會有哪些變化。惋惜的是,你會發現代碼體積由如今的219字節激增到2.1KB,即使算上代碼壓縮(未混淆)代碼也有1.1KB。轉譯後輸出的代碼長這樣:babel

"use strict";var _createClass=function(){function a(a,b){for(var c,d=0;d<b.length;d++)c=b[d],c.enumerable=c.enumerable||!1,c.configurable=!0,"value"in c&&(c.writable=!0),Object.defineProperty(a,c.key,c)}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}();function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function(){function a(b,c,d){_classCallCheck(this,a),this.id=b,this.setLocation(c,d)}return _createClass(a,[{key:"setLocation",value:function c(a,b){this.x=a,this.y=b}}]),a}();

Babel 僅僅是把咱們定義的 Shape 還原成一個 ES5 函數與對應的原型方法麼?函數

1、揭祕

好像沒那麼簡單,爲了摸清實際轉譯流程,咱們先將上述類定義代碼簡化爲一個只有14字節的空類:工具

class Shape {}

首先,當訪問器走到類聲明階段,須要補充嚴格模式:優化

"use strict";

class Shape {}

而進入變量聲明與標識符階段時則需補充 let 關鍵字並轉爲 var:ui

"use strict";

var Shape = class Shape {};

到這個時候代碼的變化都不太大。接下來是進入函數表達式階段,多出來幾行函數:this

"use strict";

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

var Shape = function Shape() {
  _classCallCheck(this, Shape);
};

該階段不只替換了 class,還在類中調用了叫作 _classCallCheck 的方法。這是什麼呢?

這個函數的做用在於確保構造方法永遠不會做爲函數被調用,它會評估函數的上下文是否爲 Shape 對象的實例,以此肯定是否須要拋出異常。接下來,則輪到 babel-plugin-minify-simplify上場,這個插件作的事情在於經過簡化語句爲表達式、並使表達式儘量統一來精簡代碼。運行後的輸出是這樣的:

"use strict";

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

var Shape = function Shape() {
  _classCallCheck(this, Shape);
};

能夠看到 if 語句中因爲只有一行代碼,因而花括號被去掉。接下來上場的即是內置的 Block Hoist ,該插件經過遍歷參數排序而後替換,Babel 輸出結果爲:

"use strict";

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

var Shape = function a() {
  _classCallCheck(this, a);
};

最後一步,minify 一下,代碼體積由最初的14字節增爲338字節:

"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var Shape=function a(){_classCallCheck(this,a)};

2、再說一些

這是一個什麼都沒幹的類聲明,但現實中任何類都會有本身的方法,而此時 Babel 一定會引入更多的插件來幫助它完成代碼的轉譯工做。直接在剛剛的空類中定義一個方法吧。

class Shape {
  render() {
      console.log("Hi");
  }
}

咱們用 Babel 轉譯一下,會發現代碼中包含以下這段:

var _createClass = function () { function a(a, b) { for (var c, d = 0; d < b.length; d++) c = b[d], c.enumerable = c.enumerable || !1, c.configurable = !0, "value" in c && (c.writable = !0), Object.defineProperty(a, c.key, c); } return function (b, c, d) { return c && a(b.prototype, c), d && a(b, d), b; }; }();

相似前面咱們遇到的 _classCallCheck ,這裏又多出一個 _createClass ,這是作什麼的呢?咱們稍微把代碼狀態往前挪一挪,來到 babel-plugin-minify-builtins 處理階段(該插件的做用在於縮減內置對象代碼體積,但咱們主要關注點在於這個階段的 _createClass 函數是基本可讀的),此時 _classCallCheck 長成這樣:

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 用於處理建立對象屬性,函數支持傳入構造函數與需定義的鍵值對屬性數組。函數判斷傳入的參數(普通方法/靜態方法)是否爲空對應到不一樣的處理流程上。而 defineProperties 方法作的事情即是遍歷傳入的屬性數組,而後分別調用 Object.defineProperty 以更新構造函數。而在 Shape 中,因爲咱們定義的不是靜態方法,咱們便這樣調用:

_createClass(Shape, [{
    key: "render",
    value: function render() {
      console.log("Hi");
    }
  }]);

T.J. Crowder 在 How does Babel.js create compile a class declaration into ES2015? 中談到 Babel 是如何將 class 轉化爲 ES5 兼容代碼時談到了幾點,大意爲:

  • constructor 會成爲構造方法數;
  • 全部非構造方法、非靜態方法會成爲原型方法;
  • 靜態方法會被賦值到構造函數的屬性上,其餘屬性保持不變;
  • 派生構造函數上的原型屬性是經過 Object.create(Base.prototype) 構造的對象,而不是 new Base() ;
  • constructor 調用構造器基類是第一步操做;
  • ES5 中對應 super 方法的寫法是 Base.prototype.baseMethod.call(this); ,這種操做不只繁瑣並且容易出錯;

這些概述大體總結了類定義在兩個 ES 版本中的一些差別,其餘不少方面好比 extends ——繼承關鍵字,它的使用則會使 Babel 在轉譯結果加上 _inherits 與 _possibleConstructorReturn兩個函數。篇幅所限,此處再也不展開詳述。

3、最後

語法糖 class 給咱們帶來了不少寫法上的便利,但可能會使咱們在代碼體積上的優化努力「付諸東流」。

另外一方面,若是你是一名 React 應用開發者,你是否已經在想將代碼中的全部 class 寫法換爲 function 呢?那樣作的話,代碼體積無疑會減小不少,但你必定也知道 PureComponent 相比 Component 的好處。因此雖然 function 給你的代碼體積減負了,但他在哪裏又給你無形增長負擔了呢?

所以,真的不推薦開發者用 class 這種寫法麼,你以爲呢?

原文連接:https://www.jianshu.com/p/ff0...

相關文章
相關標籤/搜索