JavaScript 新舊替換四:繼承

引子

在一些書籍中花費了很多的篇幅進行講述,新的語法中也出現了相關的關鍵字,實現的方式中也涉及到 JavaScript 中很重要的知識點。git

注意:JavaScript 中並無相似 Java 中的類和繼承,如下用「類」和「繼承」是爲了方便描述。es6

上一篇 JavaScript 新舊替換三:參數轉換github

ES5 方式

實現繼承功能的方式有多種,JavaScript 中經常使用的繼承模式是組合繼承,這裏以此爲例。express

function Fruit(name) {
    this.name = name;
  }

  Fruit.prototype.showName = function() {
    console.info("Fruit Name:", this.name);
  };

  function Apple(name, color) {
    Fruit.call(this, name);

    this.color = color;
  }

  Apple.prototype = new Fruit();
  // 矯正語義指向,並非必需
  Apple.prototype.constructor = Apple;

  Apple.prototype.showColor = function() {
    console.info("Apple Color:", this.color);
  };

  var apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在組合繼承中,主要的思路是:babel

  • 建立子類的時候,經過 Fruit.call(this, name) 綁定子類的 this,達到繼承父類屬性效果。
  • 將父類的實例賦給子類的 prototype 屬性,子類的實例會沿着原型鏈查找,達到了繼承父類方法的效果。

ES2015+ 方式

用新的語法實現上面的繼承:app

class Fruit {
    constructor(name) {
      this.name = name;
    }

    showName() {
      console.info("Fruit Name:", this.name);
    }
  }

  class Apple extends Fruit {
    constructor(name, color) {
      super(name);
      this.color = color;
    }

    showColor() {
      console.info("Apple Color:", this.color);
    }
  }

  let apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在書寫形式上有很大的變化,但實際上也是經過原型鏈實現,經過 Babel 轉譯爲 ES5 看下是怎樣的實現思路。函數

首先說明一下 Babel 中轉譯有兩種模式:normal 和 loose。ui

  • loose 模式下生成更簡單、兼容性更好的代碼。
  • normal 模式下生成符合標準語義的代碼。

選擇 normal 模式的轉譯更加合適,先來看下 Fruit 類轉譯後的實現:this

"use strict";
/**
 * Symbol.hasInstance 屬性,指向一個內部方法。
 * 當其它對象使用 instanceof 運算符,判斷是否爲該對象的實例時,會調用這個方法。
 * 好比,foo instanceof Foo 在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)。
 */
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) {
  // 判斷 instance 是否爲 Constructor 的實例
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

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

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

      this.name = name;
    }

    _createClass(Fruit, [
      {
        key: "showName",
        value: function showName() {
          console.info("Fruit Name:", this.name);
        }
      }
    ]);

    return Fruit;
  })();

在上面轉譯的代碼中,處理的主要思路有:prototype

  • _classCallCheck 方法判斷調用的方式,防止 Fruit() 這樣直接調用。
  • _createClass 方法在 prototype 上添加公用方法,在 Fruit 上添加靜態方法。

這種方式跟組合使用構造函數模式和原型模式建立對象很類似,不過表達的語義不太同樣。

再來看下繼承轉譯後的代碼:

"use strict";

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

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 _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

/**
 *
 * Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。
 * @param {*} superClass
 */
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 _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");
  }
}

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 Fruit =
  /*#__PURE__*/
  (function() {
    function Fruit(name) {
      _classCallCheck(this, Fruit);

      this.name = name;
    }

    _createClass(Fruit, [
      {
        key: "showName",
        value: function showName() {
          console.info("Fruit Name:", this.name);
        }
      }
    ]);

    return Fruit;
  })();

var Apple =
  /*#__PURE__*/
  (function(_Fruit) {
    _inherits(Apple, _Fruit);

    function Apple(name, color) {
      var _this;

      _classCallCheck(this, Apple);

      // _getPrototypeOf(Apple).call(this, name) 調用的實際是父類的函數,注意沒有使用 new ,返回的是默認的 undefined
      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(Apple).call(this, name)
      );
      _this.color = color;
      return _this;
    }

    _createClass(Apple, [
      {
        key: "showColor",
        value: function showColor() {
          console.info("Apple Color:", this.color);
        }
      }
    ]);

    return Apple;
  })(Fruit);

  let apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在上面轉譯的代碼中,處理的主要思路是:

  1. _inherits 方法基於父類的 prototype 建立了一個新的對象,賦給了子類的 prototype。還將子類的 __proto__ 指向了父類,爲的是繼承父類的屬性。
  2. 直接在子類的 prototype 上定義子類本身的方法。
  3. 在執行子類的構造函數時,修改了 this 的值。

ES2015+ 方式語法點

class

ES2015 引入了類的概念,經過 class 關鍵字能夠定義類。類有下面一些特色:

  1. 必需要使用 new 調用,不然會報錯。
  2. 類沒有提高,這種規定的緣由與繼承有關,必須保證子類在父類以後定義。
  3. 類中默認使用的是嚴格模式。
  4. 不想被繼承的方法加上 static 關鍵字。
  5. 類中 this 指向類的實例。

constructor

constructor 是構造方法,經過 new 命令生成對象實例時,自動調用該方法。一個類必須有 constructor 方法,若是沒有顯式定義,一個空的 constructor 方法會被默認添加。

class A {}
let obj = new A();
console.info(obj.constructor === A.prototype.constructor);

extends

class 繼承經過 extends 實現。繼承時,類中構造函數必需要執行 super 方法,不然建立實例的時候會報錯。

class A {}
class B extends A {
  constructor() {

  }
}

let obj = new B();
// VM55958:3 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

子類中若是沒有顯式的寫出構造方法,會默認的添加。

class A {}
class B extends A {}
// 等同於
class B extends A {
  constructor(...args) {
    super(...args)
  }
}

super

super 關鍵字能夠當作函數或對象使用。使用 super 的時候,必須顯式指定是做爲函數、仍是做爲對象使用,不然會報錯。

函數使用

  1. 表明父類的構造函數。
  2. 只能在子類的構造函數中使用,其它地方會報錯。

對象使用

  1. 在普通方法中,指向父類的原型對象。
  2. 在靜態方法中,指向父類。

參考資料

相關文章
相關標籤/搜索