JavaScript繼承機制演進

爲何使用繼承

繼承的本質在於更好地實現代碼的複用,這裏的代碼指的是數據與行爲的複用。數據層面咱們能夠經過對象的賦值來實現,而行爲層面,咱們能夠直接使用函數。當二者都須要被「組合」複用的時候,咱們須要經過繼承知足需求。javascript

繼承方式

原型繼承

每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個原型對象的指針。咱們將原型對象等於另外一個類型的實例,構成了實例與原型之間的鏈條。java

function SuperType() {
    this.status = true
}
SuperType.prototype.getStatus = function() {
    return this.status
}
function SubType() {
    this.subStatus = false
}
SubType.prototype.getSubStatus = function() {
    return this.subStatus
}

SubType.prototype = new SuperType()
var foo = new SubType()
複製代碼

以上代碼描述了SubType繼承SuperType的過程。經過建立SuperType的實例,並賦值給SubType.prototype來實現。express

原型鏈繼承的問題在於,若是原型中含有引用類型的值,那麼若是咱們經過實例對原型上的引用類型值進行修改,則會影響到其餘的實例。編程

function SuperType() {
    this.name = ['dog', 'cat']
}
function SubType() {}

SubType.prototype = new SuperType()

var instance1 = new SubType()
instance1.name.push('fish')

var instance2 = new SubType()
console.log(instance2.name) // ['dog', 'cat', 'fish']
複製代碼

能夠看出,全部的實例都共享了name這一屬性,經過對於instance1的修改從而影響到了instance2.name。該例子中須要注意的地方在於,instance1.name.push('fish')其實是經過實例對象保存了了對SuperType的name屬性的引用來完成操做。這裏要注意的是,若是實例上存在與原型上同名的屬性,那麼原型中的屬性會被屏蔽,針對該屬性的修改則不會影響到其餘的實例。bash

借用構造函數繼承

經過在子類構造函數中調用父類的構造函數,可使用call、apply方法來實現。babel

function Super() {
    this.name = ['Mike', 'David']
}
Super.prototype.addname = function (name) {
    this.name.push(name)
}
function Sub() {
    Super.call(this)
}
var foo = new Sub()
foo.name // ['Mike', 'David']
複製代碼

相比於原型鏈而言,這種繼承方法能夠在子類構造函數中調用父類構造函數時傳遞參數。可是借用構造函數繼承仍然有如下問題:app

  • 只可以繼承父類的實例的屬性和方法,沒法繼承原型屬性與方法
  • 沒法複用,每一個子類都有父類實例函數的副本,沒法實現函數的複用。

組合繼承

使用原型鏈方式繼承原型屬性與方法,使用借用構造函數方法來實現對於實例屬性的繼承。ide

function Super() {
  this.name = ['Mike', 'David']
}
Super.prototype.addname = function (name) {
  this.name.push(name)
}
function Sub() {
  Super.call(this)
}
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
Sub.prototype.getName = function() {
  console.log(this.name.join(','))
}
var foo = new Sub()
複製代碼

這種繼承方式集合了原型鏈與借用構造函數方法的優點,既保證了函數方法的複用,同時也保證了每一個實例都有本身的屬性。可是,這種繼承方式的侷限在於:建立實例對象時,原型中會存在兩份相同的屬性、方法。函數

原型式繼承

基本的思想是藉助原型基於已有的對象來建立新的對象。oop

function extend(obj) {
    function noop() {}
    noop.prototype = obj
    return new noop()
}

var Animals = {
    name: 'animal',
    type: ['dog', 'cat', 'bird']
}
var anotherAnimals = extend(Animals)
anothierAnimals.type.push('horse')

var yetAnotherAnimal = extend(Animals)
yetAnotherAnimal.type.push('whale')

console.log(Animals.type) // ['dog', 'cat', 'bird', 'horse', 'whale']
複製代碼

從上述例子中能夠看出,咱們選擇了Animal做爲基礎傳遞給extend方法,該方法返回的對新對象。在ES5中,新增了Object.create()方法。這個方法接受兩個參數,一個是用做新對象原型的對象,另外一個是爲新對象定義額外屬性的對象(可選)。該方法若是隻傳第一個參數的狀況下,的行爲與上述代碼中extend方法相同。

// 同Object.create改寫上面的代碼
var Animals = {
    name: 'animal',
    type: ['dog', 'cat', 'bird']
}
var anotherAnimals = Object.create(Animals)
anothierAnimals.type.push('horse')
複製代碼

因爲Animals中含有引用類型的屬性(type),所以存在繼承多個實例引用類型屬性指向相同,有篡改問題的狀況。而且,該繼承方式沒法傳遞參數。

寄生式繼承

在原型式繼承的基礎上,經過爲構造函數新增屬性和方法,來加強對象。

function cusExtend(obj) {
    var clone = extend(obj)
    clone.foo = function() {
        console.log('foo')
    }
    return clone
}
var Animals = {
    name: 'animal',
    type: ['dog', 'cat', 'bird']
}
var instance = cusExtend(Animals)
instance.foo() // foo
複製代碼

將結果賦值給clone以後,再爲clone對象添加了一個新的方法。此方式缺陷與原型式繼承相同,同時也沒法實現函數的複用。

寄生組合式繼承

結合借用構造函數繼承屬性方法,寄生式繼承方法繼承原型方法。

function SuperType(name) {
    this.name = name
}
function SubType(name, age) {
    SuperType.call(this, name, age)
    this.age = age
}
SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype.constructor = SubType

var foo = new SubType('Mike', 16)
複製代碼

對比與組合繼承中SubType.prototype = new SuperType(),這個步驟實際上會是給SubType.prototype增長了name屬性,而在調用SuperType.call(this, name, age)時,SubType的name屬性屏蔽了其原型上的同名name屬性。這即是組合繼承的一大問題--會調用兩次超類的構造函數,而且在原型上產生同名屬性的冗餘。

在寄生組合式繼承中,Object.create()方法用於執行一個對象的[[prototype]]指定爲某個對象。SubType.prototype = Object.create(SuperType.prototype) 至關於 SubType.prototype._proto_ = SuperType.prototype。該方法可簡化爲下面函數:

function objectCeate(o) {
    var F = function() {}
    var prototype = o.prototype
    F.prototype = prototype
    return new F()
}
複製代碼

這裏僅僅調用了一次SuperType構造函數,而且避免了在SubType.prototype上建立沒必要要的、多餘的屬性。這也是該繼承方法相比於上述其他方法的優點所在,是一個理想的繼承方法。

Class的繼承

咱們先來看將上述寄生組合式繼承的例子改寫爲class繼承的方式。Class經過extends關鍵字來實現繼承。

class SuperType {
    constructor(name) {
        this.name = name
    }
}

class SubType extends SuperType {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}

var foo = new SubType()
複製代碼

其中Super關鍵字至關於進行了SuperType.call(this)的操做。

上面的兩種方式的有一條相同的原型鏈:

foo.__proto__ => SubType.prototype 
SubType.prototype.__proto__ => SuperType.prototype
複製代碼

區別在於,class繼承的方式多了一條繼承鏈,用於繼承父類的靜態方法與屬性:

SubType.__proto__ => SuperType
複製代碼

將上述兩條鏈梳理一下獲得:

  1. 子類的prototype屬性的__proto__表示方法的繼承,老是指向父類的prototype
  2. 子類的__proto__屬性,表示構造函數的繼承,老是指向父類
class A {
}

class B extends A {
}

// B繼承A的靜態屬性
Object.setPrototypeOf(B, A)// B.__proto__ === A 

// B的實例繼承A的實例
Object.setPrototypeOf(B.prototype, A.prototype) // B.prototype.__proto__ === A.prototype 

複製代碼

其中Object.setPrototypeOf方法用於指定一個對象的[[prototype]],可簡化實現爲:

Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
  obj.__proto__ = proto;
  return obj; 
}
複製代碼

經過babel編譯以上繼承代碼,能夠獲得:

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

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

var SuperType = function SuperType(name) {
  _classCallCheck(this, SuperType)

  this.name = name
}

var SubType =
  /*#__PURE__*/
  (function(_SuperType) {
    _inherits(SubType, _SuperType)

    function SubType(name, age) {
      var _this

      _classCallCheck(this, SubType)

      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(SubType).call(this, name)
      )
      _this.age = age
      return _this
    }

    return SubType
  })(SuperType)

var foo = new SubType()

複製代碼

咱們挑出其中關鍵的代碼片斷來看:

_inherits

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)
}
複製代碼

首先是針對superClass的類型作了判斷,只容許是function與null類型,不然拋出錯誤。能夠看出其繼承的方法相似於寄生組合繼承的方式。最後利用了setPrototypeOf的方法來繼承了父類的靜態屬性。

_possibleConstructorReturn

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
}

...

 _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(SubType).call(this, name)
      )
複製代碼

首先咱們來看調用的方式,傳入了兩個參數,getPrototypeOf方法能夠用來從子類上獲取父類。咱們這裏能夠簡化看作是_possibleConstructorReturn(this, SuperType.call(this, name))。這裏因爲SuperType.call(this, name)返回是undefined,咱們繼續走到_assertThisInitialized方法,返回了self(this)。

結合代碼

function SubType(name, age) {
    var _this;

    _classCallCheck(this, SubType);

    _this = _possibleConstructorReturn(this, _getPrototypeOf(SubType).call(this, name));
    _this.age = age;
    return _this;
  }
複製代碼

能夠看出,ES5的繼承機制是在子類實例對象上創造this,在將父類的方法添加在this上。而在ES6中,本質是先將父類實例對象的屬性與方法添加在 this上(經過super),而後再用子類的構造函數修改this(_this.age = age)。所以,子類必須在constructor中調用super方法,不然新建實例會報錯。

整個繼承過程咱們能夠梳理爲如下步驟:

  1. 執行_inherits方法,創建子類與父類之間的原型鏈關係。相似於寄生組合繼承中的方式,不一樣的地方在於額外有一條繼承鏈:SubType.__proto__ = SuperType
  2. 接着調用_possibleConstructorReturn方法,根據父類構造函數的返回值來初始化this,在調用子類的構造函數修改this。
  3. 最終返回子類中的this

擴展:constructor指向重寫

經過上述的代碼,咱們會觀察到組合繼承與class繼承中都有contructor指向的重寫。

// class
subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  })
  
// 組合繼承
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
複製代碼

咱們知道原型對象(即prototype這個對象)上存在一個特別的屬性,即constructor,這個屬性指向的方法自己。

若是咱們嘗試去註釋掉修正contructor方法指向的代碼後,運行的結果實際上是不受影響的。

經過查詢,知道了一篇回答What it the significance of the Javascript constructor property?

The constructor property makes absolutely no practical difference to anything internally. It's only any use if your code explicitly uses it. For example, you may decide you need each of your objects to have a reference to the actual constructor function that created it; if so, you'll need to set the constructor property explicitly when you set up inheritance by assigning an object to a constructor function's prototype property, as in your example.

能夠看出,咱們若是不這樣作不會有什麼影響,可是在一種狀況下 -- 咱們須要顯式地去調用構造函數。好比咱們想要實例化一個新的對象,能夠藉助去訪問已經存在的實例原型上的constructor來訪問到。

// 組合繼承
function Super(name) {
  this.name = name
}
Super.prototype.addname = function (name) {
  this.name.push(name)
}
function Sub(age) {
  Super.call(this, name)
  this.age = age || 3
}
Sub.prototype = new Super()
// Sub.prototype.constructor = Sub
Sub.prototype.getName = function() {
  console.log(this.name.join(','))
}
// 假設此時已經存在一個Sub的實例foo,此時咱們想構造一個新的實例foo2
var foo2 = new foo.__proto__.constructor()
console.log(foo2.age) // undefined
複製代碼

咱們能夠看到因爲註釋了constructor相關的代碼,以致於Sub.prototype.constructor實際上指向爲Super,所以foo2.age的值是undefined。

另外引用知乎上賀師俊的回答

constructor其實沒有什麼用處,只是JavaScript語言設計的歷史遺留物。因爲constructor屬性是能夠變動的,因此未必真的指向對象的構造函數,只是一個提示。不過,從編程習慣上,咱們應該儘可能讓對象的constructor指向其構造函數,以維持這個慣例。

相關文章
相關標籤/搜索