從 Babel 轉譯過程淺談 ES6 實現繼承的原理

都說 ES6 的 Class 是 ES5 的語法糖,那麼 ES6 的 Class 是如何實現的呢?其實現繼承的原理又是什麼呢?不妨咱們經過 Babel 轉譯代碼的方式,看看其中有什麼門道。express

這篇文章會從最簡單的代碼入手,一步步剖析相關的原理以及每一個函數的做用。代碼的轉譯直接在 Babel 官網進行便可。數組

ES6 的 Class 是如何實現的

先從最簡單的一個 Parent 類看起:babel

class Parent{
    constructor(){
        this.a = 1
           this.getA = function(){}
   }
}

轉譯以後的結果是:app

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

var Parent = function Parent() {
    "use strict";

    _classCallCheck(this, Parent);
    this.a = 1;
    this.getA = function () {};
};

能夠看到,這裏的類實質上就是 ES5 中的構造函數,除了添加實例屬性和實例方法以外,它還調用了一個 _classCallCheck 函數。函數

_classCallCheck 函數

這個函數會接受一個實例和構造函數做爲參數,內部的 instance instanceof Constructor 用於判斷這個類是否是經過 new 調用的,若是不是就拋出一個錯誤。工具

接下來咱們嘗試給這個類添加原型方法和靜態方法:post

class Parent{
   constructor(){
     this.a = 1
     this.getA = function(){}
   }
   getB(){}
   getC(){}
   static getD(){}
   static getE(){}
}

轉譯後獲得:this

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a 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);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Parent = /*#__PURE__*/ (function () {
  "use strict";

  function Parent() {
    _classCallCheck(this, Parent);

    this.a = 1;

    this.getA = function () {};
  }

  _createClass(
    Parent,
    [
      {
        key: "getB",
        value: function getB() {}
      },
      {
        key: "getC",
        value: function getC() {}
      }
    ],
    [
      {
        key: "getD",
        value: function getD() {}
      },
      {
        key: "getE",
        value: function getE() {}
      }
    ]
  );

  return Parent;
})();

emmm 看起來好像有點複雜,不過不要緊,咱們一個一個函數理清楚就好了。prototype

能夠看到,此時的 Parent 變成了一個 IIFE,IIFE 執行以後仍然是返回 Parent 類,但內部還封裝了一個 _createClass 函數的調用。code

_createClass 函數

_createClass 函數作了什麼事呢?首先,它能夠接受三個參數:

  • 第一個參數: 類(這裏是 Parent 類)
  • 第二個參數:存放對象的數組,每一個對象都是關於類的原型方法的特性描述對象(這裏是 getBgetC
  • 第三個參數:存放對象的數組,每一個對象都是關於類的靜態方法的特性描述對象(這裏是 getDgetE

接着,它會依次檢查是否有傳第二個和第三個參數,若是有,就調用 _defineProperties 函數,分別爲類的原型定義原型方法,爲類自己定義靜態方法。

_defineProperties 函數

_defineProperties 函數作了什麼事呢?它接受類(或者類的原型)和一個存放對象的數組做爲參數,以後遍歷數組中的每一個對象,定義每一個方法的特性,並將它們逐一添加到類(或者類的原型)上面。這裏涉及到的特性包括:

  • enumberable:該屬性(方法)是否可枚舉。若是方法自己已經定義了該特性,則採用該特性;若是沒有定義,則定義該方法爲不可枚舉
  • configurable:該屬性(方法)是否能夠配置
  • writable:若是該屬性是數據屬性而不是訪問器屬性,那麼會有一個 value,此時設置該屬性爲可寫

ES6 的繼承是如何實現的

好了,基本搞清楚一個 class 的原理以後,如今咱們來看一下 ES6 是如何實現繼承的。

將下面的代碼進行轉譯:

class Parent{
   constructor(){
     this.a = 1 
     this.getA = function(){} 
   }
   getB(){}  
   getC(){}
   static getD(){}
   static getE(){}
}
class Son extends Parent{
    constructor(){
        super()        
    }
}

就獲得了:

"use strict";

function _typeof(obj) {
  "@babel/helpers - typeof";
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

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

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
    return true;
  } catch (e) {
    return false;
  }
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a 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);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Parent = /*#__PURE__*/ (function () {
  function Parent() {
    _classCallCheck(this, Parent);

    this.a = 1;

    this.getA = function () {};
  }

  _createClass(
    Parent,
    [
      {
        key: "getB",
        value: function getB() {}
      },
      {
        key: "getC",
        value: function getC() {}
      }
    ],
    [
      {
        key: "getD",
        value: function getD() {}
      },
      {
        key: "getE",
        value: function getE() {}
      }
    ]
  );

  return Parent;
})();

var Son = /*#__PURE__*/ (function (_Parent) {
  _inherits(Son, _Parent);

  var _super = _createSuper(Son);

  function Son() {
    _classCallCheck(this, Son);

    return _super.call(this);
  }

  return Son;
})(Parent);

emmm 好像愈來愈複雜了,沒事,咱們先稍稍簡化一下(前面解釋過的函數這裏就直接略過了),再一個一個慢慢分析:

"use strict";

function _typeof(obj) { ... }

function _inherits(subClass, superClass) { ... }

function _setPrototypeOf(o, p) { ... }

function _createSuper(Derived) { ... }

function _possibleConstructorReturn(self, call) { ... }

function _assertThisInitialized(self) { ... }

function _isNativeReflectConstruct() { ... }

function _getPrototypeOf(o) { ... }

function _classCallCheck() { ... }

function _defineProperties() { ... }

function _createClass() { ... }

var Parent = /*#__PURE__*/ (function () {
  function Parent() { ... }

  _createClass(...);

  return Parent;
})();

var Son = /*#__PURE__*/ (function (_Parent) {
  _inherits(Son, _Parent);

  var _super = _createSuper(Son);

  function Son() {
    _classCallCheck(this, Son);

    return _super.call(this);
  }

  return Son;
})(Parent);

這裏多出了不少新的函數,有的函數不是咱們討論的重點,並且也徹底能夠單獨拎出來分析,因此這裏先簡單把它們的做用介紹了,以後若是忘記了函數的做用,翻到這裏來看便可。

_typeof(obj)
function _typeof(obj) {
  "@babel/helpers - typeof";
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

這是 Babel 引入的一個工具函數,主要是爲了對 Symbol 進行正確的處理。它首先會檢查當前環境是否支持原生的 Symbol,若是支持就直接返回 typeof obj 表達式的計算結果;若是不支持,再檢查 obj 是否是經過 polyfill 實現的 Symbol 的一個實例,若是是就返回它的類型(也就是返回 "symbol"),若是不是,就返回 typeof obj 的計算結果。在這裏,這個函數假定了咱們當前的環境是原生支持 Symbol 或者經過 polyfill 實現了支持的。

_setPrototypeOf()
function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

首先檢查當前環境是否支持直接調用 Object.setPrototypeOf() 方法,若是不支持,就經過 __proto__ 手動給實例創建原型關係( __proto__ 是一個暴露的內部屬性,通常不提倡直接進行操做)。

_possibleConstructorReturn(self,call)
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

若是你看過 new 或者 [[Construct]] 的內部實現,就會知道,給構造函數指定了一個非空對象或者函數做爲返回值以後,調用函數以後返回的將不是實例,而是這個對象或者函數。這裏就是經過 _possibleConstructorReturn 這個函數來完成這件事的 —— 仔細看它的名字,意思不就是「構造函數可能返回的值」嗎?

這個函數接受兩個參數,self 表明構造函數的實例,call 表明構造函數的返回值。內部的判斷也很簡單,call && (_typeof(call) === "object" || typeof call === "function") 是檢查 call 的類型,當它是一個對象(注意這裏是使用 typeof 進行檢查,須要排除可能爲 null 的狀況)或者函數的時候,直接將其做爲返回值;不然就返回 _assertThisInitialized(self)。等等,怎麼又來了一個新函數呢?不要急,咱們接着就來看這個函數是幹什麼用的。

_assertThisInitialized(self)
function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

看這個函數的名字 —— 「斷言 this 已經初始化」,也就是說,在調用這個方法的時候,咱們指望的結果是 this 已經獲得初始化了。這裏若是檢查發現 thisundefined,就會拋出一個錯誤,提示咱們因爲沒有調用 super(),因此沒法獲得 this;不然就返回 this 。爲何要使用 void 0 而不是 undefined 呢?由於非嚴格模式下 undefined 可能會被重寫,這裏使用 void 0 更加保險。

_isNativeReflectConstruct()
function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
    return true;
  } catch (e) {
    return false;
  }
}

這個方法用於檢測當前環境是否支持原生的 Reflect。爲何要作這個檢查呢?後面咱們再來解釋。

好了,咱們已經分析了這幾個函數的做用,如今直接翻到最下面的代碼,從 Son 子類看起:

var Son = /*#__PURE__*/ (function (_Parent) {
  _inherits(Son, _Parent);

  var _super = _createSuper(Son);

  function Son() {
    _classCallCheck(this, Son);

    return _super.call(this);
  }

  return Son;
})(Parent);

這裏的 Son 一樣是一個 IIFE,而且實際上也是返回一個 Son 子類構造函數,不一樣的是,它內部還封裝了其它方法的調用。咱們逐一看一下這些方法的做用。

_inherits(Son,_Parent)
function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { 
        value: subClass, 
        writable: true, 
        configurable: true 
    }
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

_inherit 是實現繼承的其中一個核心方法,能夠說它的本質就是 ES5 中的寄生組合式繼承。這個方法接受一個父類和子類做爲參數,首先會檢查父類是否是函數或者 null,若是不是,則拋出錯誤(爲何父類能夠是 null從 extends 看 JS 繼承這篇文章進行了解釋,這裏我就不重複了)。

接着,調用 Object.create 設置父類的原型爲子類原型的 __proto__。這裏咱們會看到還傳入了第二個參數,這個參數是子類原型的屬性的特性描述對象(descriptor),咱們對 constructor 屬性進行了設置,將它設置爲可寫、可配置,同時利用 value 修復了因重寫子類原型而丟失的 constructor 指向。爲何不設置 enumerable: false 呢?由於默認就是不可枚舉的,不設置也行。

最後,咱們設置子類的 __proto__ 指向父類,這是 ES5 中沒有的,目的是讓子類繼承父類的靜態方法(能夠直接經過類調用的方法)。

能夠看到,經過調用 _inherit 函數,咱們已經成功讓子類繼承了父類的原型方法和靜態方法。不過,實例上的屬性怎麼繼承呢?這就要繼續往下看了,接下來咱們調用 _createSuper() 函數並傳入派生類(子類),這不是重點,重點是它建立並返回的另外一個函數 _super

_super.call(this)
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}

這裏的 _createSuperInternal 就是 _super,調用的時候咱們綁定了其內部的 this 爲子類實例。

它首先會根據以前的 _isNativeReflectConstruct 檢查當前環境是否支持 Reflect,若是支持,則執行 result = Reflect.construct(Super, arguments, NewTarget),不然執行 result = Super.apply(this, arguments)

解釋一下這裏爲何要優先使用 Reflect。當執行 Reflect.construct(Super, arguments, NewTarget)的時候,最終會返回一個基於 Super 父類構造函數建立的實例,至關於執行了 new Super(...arguments),可是,這個實例的 __proto__constructorNewTarget,所以在某種程度上,你也能夠說這就是一個子類實例,不過它擁有父類實例的全部屬性。

可能你會說,這和下面的 Super.apply (借用構造函數繼承)不是沒區別嗎?非也。咱們使用 Super.apply 的時候,其實 new.target 屬性是會丟失的,就像下面這樣:

function Super(){
    console.log(new.target)
}
new Super()       // Super
Super.apply({})   // undefined

可是若是使用 Reflect.consturct 來建立對象,則 new.target 不會丟失:

function Super1(){
    console.log('Super1')
    console.log(new.target)
}
function Super2(){
    console.log('Super2')
    console.log(new.target)
}
const obj1 = Reflect.construct(Super1,{})              
// 'Super1'
//  Super1
const obj2 = Reflect.construct(Super1,{},Super2)
// 'Super1'
//  Super2

能夠看到,即使沒有經過 new 去調用 Super1new.target 也仍然指向 Super1;而在傳了第三個參數以後,new.target 也沒有丟失,只是指向了 Super2(前面咱們說過了,某種程度上,能夠說 obj1 就是 Super2 的實例)。

因此,這裏優先使用 Reflect,是爲了保證 new.target 不會丟失。

以後,result 可能有三種取值:

  • 一個繼承了父類實例全部屬性的子類實例
  • 父類構造函數的調用結果,多是父類構造函數中自定義返回的一個非空對象
  • 父類構造函數的調用結果,多是默認返回的 undefined

如何處理這些不一樣的狀況呢?這裏調用了前面講過的 _possibleConstructorReturn(this,result)函數,若是判斷 result 是一個非空對象,也就是第一種和第二種取值狀況,那麼就直接返回 result;不然就是第三種狀況了,此時就對當初傳進去的子類實例(已經經過 Super.apply 對它進行了加強),也就是 this,進行斷言,而後返回出去。

如今,讓咱們再回到 Son 構造函數。能夠看到,調用它以後返回的正是 _super.call(this),也就是返回 result 或者通過加強的this。這裏的 result 咱們知道也有兩種取值,若是是一個繼承了父類實例全部屬性的子類實例,那麼實際上等價於通過加強的 this;若是是父類構造函數中自定義返回的一個非空對象,則意味着調用 Son構造函數以後返回的對象實際上並無繼承父類中聲明的實例屬性。相似下面這樣:

function Parent(){
    this.a = 1
    return {b:1}
}
function Son(){
    return Parent.call(this)
}
Son.prototype.__proto__ = Parent.prortotype
const obj = new Son()
console.log(obj)     
// {b:1}      
// 這裏 `Son` 一樣也是返回父類 `Parent` 的調用結果(一個對象),它並無繼承在父類上聲明的實例屬性 `a`。

到這裏,咱們的分析基本就結束了。但願你閱讀完本文以後有所收穫,若發現文章有錯誤,也歡迎評論區指正。

相關文章
相關標籤/搜索