從 Prototype 開始提及(下)—— ES6 中的 class 與 extends

何爲 class

衆所周知,JavaScript是沒有類的,class也只是語法糖,這篇文章旨在於理清咱們經常掛着嘴邊的語法糖,究竟指的是什麼。javascript

ES6ES5 寫法對比

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

這是一個很完整的寫法,咱們已經習慣於這麼方便地寫出一個類了,那麼對應到 ES5 中的寫法又是如何呢java

function Parent(name) {
    this.name = name
    this.isAdult = true
}

Parent.nation = 'China'
Parent.live = function() {
    console.log('live')
}
Parent.prototype = {
    get thought() {
        return this._thought
    },
    set thought(newVal) {
        this._thought = newVal
    },
    talk: function() {
        console.log('talk')
    }
}

能夠很清晰地看到react

  • ES6Parent 類的 constructor 對應的就是 ES5 中的構造函數 Parent
  • 實例屬性 nameisAdult,不管在 ES6 中採用何種寫法,在 ES5 中依然都是掛在 this 下;
  • ES6 中經過關鍵字 static 修飾的靜態屬性和方法 nationlive,則都被直接掛在類 Parent 上;
  • 值得注意的是 getter 和 setter tought 和 方法 talk 是被掛在 原型對象 Parent.prototype 上的。

Babel 是如何進行編譯的

咱們能夠經過將代碼輸入到 Babel 官網的 Try it out 來查看編譯後的代碼,這個部分咱們按部就班,一步一步來進行編譯,拆解 Babel 的編譯過程:git

過程一

咱們此時只觀察 屬性 相關的編譯結果,
編譯前:es6

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    constructor(name) {
        this.name = name
    }
}

編譯後:github

'use strict'
  // 封裝後的 instanceof 操做
  function _instanceof(left, right) {
    if (
      right != null &&
      typeof Symbol !== 'undefined' &&
      right[Symbol.hasInstance]
    ) {
      return !!right[Symbol.hasInstance](left)
    } else {
      return left instanceof right
    }
  }
  // ES6 的 class,必須使用 new 操做來調用,
  // 這個方法的做用就是檢查是否經過 new 操做調用,使用到了上面封裝的 _instanceof 方法
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }
  // 封裝後的 Object.defineProperty
  function _defineProperty(obj, key, value) {
    if (key in obj) {
      Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
      })
    } else {
      obj[key] = value
    }
    return obj
  }

  var Parent = function Parent(name) {
    // 檢查是否經過 new 操做調用
    _classCallCheck(this, Parent)
    // 初始化 isAdult
    _defineProperty(this, 'isAdult', true)
    // 根據入參初始化 name
    this.name = name
  }
  // 初始化靜態屬性 nation
  _defineProperty(Parent, 'nation', 'China')

從編譯後的代碼中能夠發現,Babel 爲了其嚴謹度,封裝了一些方法,其中 可能有點迷惑的是 _instanceof(left, right) 這個方法裏的 Symbol.hasInsance,從 MDNECMAScript6入門 中能夠知道,這個屬性能夠用來自定義 instanceof 操做符在某個類上的行爲。這裏還有一個重點關注對象 _classCallCheck(instance, Constructor) ,這個方法用來檢查是否經過 new 操做調用。express

過程二

編譯前:segmentfault

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

編譯後:babel

'use strict'
  // 封裝後的 instanceof 操做
  function _instanceof(left, right) {
    // .....
  }
  // ES6 的 class,必須使用 new 操做來調用,
  // 這個方法的做用就是檢查是否經過 new 操做調用,使用到了上面封裝的 _instanceof 方法
  function _classCallCheck(instance, Constructor) {
    // ......
  }
  // 封裝 Object.defineProperty 來添加屬性
  function _defineProperties(target, props) {
    // 遍歷 props
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i]
      // enumerable 默認爲 false
      descriptor.enumerable = descriptor.enumerable || false
      descriptor.configurable = true
      if ('value' in descriptor) descriptor.writable = true
      Object.defineProperty(target, descriptor.key, descriptor)
    }
  }
  // 爲 Constructor 添加原型屬性或者靜態屬性並返回
  function _createClass(Constructor, protoProps, staticProps) {
    // 若是是原型屬性,添加到原型對象上
    if (protoProps) _defineProperties(Constructor.prototype, protoProps)
    // 若是是靜態屬性,添加到構造函數上
    if (staticProps) _defineProperties(Constructor, staticProps)
    return Constructor
  }
  // 封裝後的 Object.defineProperty
  function _defineProperty(obj, key, value) {
    // ......
  }

  var Parent =
    /*#__PURE__*/
    (function() {
      // 添加 getter/setter
      _createClass(Parent, [
        {
          key: 'thought',
          get: function get() {
            console.log('Thought in head is translate to Chinese.')
            return this._thought
          },
          set: function set(newVal) {
            this._thought = newVal
          }
        }
      ])

      function Parent(name) {
        // 檢查是否經過 new 操做調用
        _classCallCheck(this, Parent)
        // 初始化 isAdult
        _defineProperty(this, 'isAdult', true)
        // 根據入參初始化 name
        this.name = name
      }
      // 添加 talk 和 live 方法
      _createClass(
        Parent,
        [
          {
            key: 'talk',
            value: function talk() {
              console.log('talk')
            }
          }
        ],
        [
          {
            key: 'live',
            value: function live() {
              console.log('live')
            }
          }
        ]
      )

      return Parent
    })()
  // 初始化靜態屬性 nation
  _defineProperty(Parent, 'nation', 'China')

與過程一相比,編譯後的代碼, Babel 多生成了一個 _defineProperties(target, props)_createClass(Constructor, protoProps, staticProps) 的輔助函數,這兩個主要用來添加原型屬性和靜態屬性,而且經過 Object.defineProperty 的方法,對數據描述符存取描述符均可以進行控制。
值得注意的是,ES6 中的 class 裏的全部方法都是不可遍歷的(enumerable: false),這裏有一個小細節: 若是有使用 TypeScript,在設置 compileOptions 中的 target 時,若是設置爲 es5,那麼會發現編譯後的 方法能夠經過 Object.keys() 遍歷到,而設置爲es6時就沒法被遍歷。函數

總結

Babel 經過 AST 抽象語法樹分析,而後添加如下

  • _instanceof(left, right) // 封裝後的 instanceof 操做
  • _classCallCheck(instance, Constructor) // 檢查是否經過 new 操做調用
  • _defineProperties(target, props) // 封裝 Object.defineProperty 來添加屬性
  • _createClass(Constructor, protoProps, staticProps) // 爲 Constructor 添加原型屬性或者靜態屬性並返回
  • _defineProperty(obj, key, value) // // 封裝後的 Object.defineProperty

五個輔助函數,來爲 Parent 構造函數添加屬性和方法,轉換 名爲 class 的語法糖爲 ES5 的代碼。

何爲 extends

既然 ES6 沒有類,那又應該如何實現繼承呢,相信聰明的你已經知道了,其實和 class 同樣,extends 也是語法糖,接下來咱們一步一步接着把這層語法糖也拆開。

ES5 的 寄生組合式繼承

從 Prototype 開始提及(上)—— 圖解 ES5 繼承相關 這裏知道,相對完美的繼承實現是 寄生組合式繼承,爲了方便閱讀,這裏再次附上源碼和示意例圖:

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Parent(name) {
    this.name = name
}

function Child(name) {
    Parent.call(this, name)
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

var child = new Child('child')

QQ20191117-204128.png

ES6ES5 寫法對比

若是參考上面的繼承實現,咱們能夠輕鬆地寫出兩種版本的繼承形式

class Child extends Parent {
    constructor(name, age) {
        super(name); // 調用父類的 constructor(name)
        this.age = age;
    }
}
function Child (name, age) {
    Parent.call(this, name)
    this.age = age
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

Babel 是如何進行編譯的

一些細節

  • 子類必須在 constructor 方法中調用 super 方法,不然新建實例時會報錯。這是由於子類沒有本身的 this 對象,而是繼承父類的 this 對象,而後對其進行加工。若是不調用 super 方法,子類就得不到 this 對象。

也正是由於這個緣由,在子類的構造函數中,只有調用 super 以後,纔可使用 this 關鍵字,不然會報錯。

  • ES6 中,父類的靜態方法,能夠被子類繼承。class 做爲構造函數的語法糖,同時有 prototype 屬性和 __proto__ 屬性,所以同時存在兩條繼承鏈。

QQ20191117-204140@2x.png

編譯過程

一樣的,咱們將代碼輸入到 Babel 官網的 Try it out 來查看編譯後的代碼:

'use strict'
  // 封裝後的 typeof
  function _typeof(obj) {
    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)
  }
  // 調用父類的 constructor(),並返回子類的 this
  function _possibleConstructorReturn(self, call) {
    if (
      call &&
      (_typeof(call) === 'object' || typeof call === 'function')
    ) {
      return call
    }
    return _assertThisInitialized(self)
  }
  // 檢查 子類的 super() 是否被調用
  function _assertThisInitialized(self) {
    if (self === void 0) {
      throw new ReferenceError(
        "this hasn't been initialised - super() hasn't been called"
      )
    }
    return self
  }
  // 封裝後的 getPrototypeOf
  function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf
      ? Object.getPrototypeOf
      : function _getPrototypeOf(o) {
          return o.__proto__ || Object.getPrototypeOf(o)
        }
    return _getPrototypeOf(o)
  }
  // 實現繼承的輔助函數
  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)
  }
  // 封裝後的 setPrototypeOf
  function _setPrototypeOf(o, p) {
    _setPrototypeOf =
      Object.setPrototypeOf ||
      function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
    return _setPrototypeOf(o, p)
  }
  // 檢查是否經過 new 操做調用
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }

  var Child =
    /*#__PURE__*/
    (function(_Parent) {
      // 繼承操做
      _inherits(Child, _Parent)

      function Child(name, age) {
        var _this

        _classCallCheck(this, Child)
        // 調用父類的 constructor(),並返回子類的 this
        _this = _possibleConstructorReturn(
          this,
          _getPrototypeOf(Child).call(this, name)
        )
        // 根據入參初始化子類本身的屬性
        _this.age = age
        return _this
      }

      return Child
    })(Parent)

_inherits(subClass, superClass)

咱們來細看一下這個實現繼承的輔助函數的細節:

function _inherits(subClass, superClass) {
    // 1. 檢查 extends 的繼承目標(即父類),必須是函數或者是 null
    if (typeof superClass !== 'function' && superClass !== null) {
      throw new TypeError(
        'Super expression must either be null or a function'
      )
    }
    // 2. 相似於 ES5 的寄生組合式繼承,使用 Object.create,
    //    設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性
    subClass.prototype = Object.create(superClass && superClass.prototype, {
      constructor: { value: subClass, writable: true, configurable: true }
    })
    // 3. 設置子類的 __proto__ 屬性指向父類
    if (superClass) _setPrototypeOf(subClass, superClass)
  }

這個方法主要分爲3步,其中第2步,經過寄生組合式繼承在實現繼承的同時,新增了一個名爲 constructor 的不可枚舉的屬性;第3步實現了上文說的第二條原型鏈,從而達到靜態方法也能被繼承的效果。

_possibleConstructorReturn(self, call)

這個輔助函數主要是用來實現 super() 的效果,對應到寄生組合式繼承上則是借用構造函數繼承的部分,有所不一樣的是,該方法返回一個 this 並賦給子類的 this。具體細節能夠在 ES6 系列之 Babel 是如何編譯 Class 的(下) 查看。

總結

class 同樣,Babel 經過 AST 抽象語法樹分析,而後添加一組輔助函數,在我看來能夠分爲兩類,第一類:

  • _typeof(obj) // 封裝後的 typeof
  • _getPrototypeOf(o) // 封裝後的 getPrototypeOf
  • _setPrototypeOf(o, p) // 封裝後的 setPrototypeOf

這種爲了健壯性的功能輔助函數
第二類:

  • _assertThisInitialized(self) // 檢查 子類的 super() 是否被調用
  • _possibleConstructorReturn(self, call) // 調用父類的 constructor(),並返回子類的 this
  • _classCallCheck(instance, Constructor) // 檢查是否經過 new 操做調用
  • _inherits(subClass, superClass) // 實現繼承的輔助函數

這種爲了實現主要功能的流程輔助函數,從而實現更完善的寄生組合式繼承

後記

從 Prototype 開始提及 一共分爲兩篇,從兩個角度來說述 JavaScript 原型相關的內容。

參考資料

相關文章
相關標籤/搜索