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指向其构造函数,以维持这个惯例。

相关文章
相关标签/搜索