JavaScript继承机制演进

为什么使用继承

继承的本质在于更好地实现代码的复用,这里的代码指的是数据与行为的复用。数据层面我们可以通过对象的赋值来实现,而行为层面,我们可以直接使用函数。当两者都需要被“组合”复用的时候,我们需要通过继承满足需求。

(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

继承方式

原型继承

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。我们将原型对象等于另一个类型的实例,构成了实例与原型之间的链条。

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来实现。

原型链继承的问题在于,如果原型中含有引用类型的值,那么如果我们通过实例对原型上的引用类型值进行修改,则会影响到其他的实例。

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属性的引用来完成操作。这里要注意的是,如果实例上存在与原型上同名的属性,那么原型中的属性会被屏蔽,针对该属性的修改则不会影响到其他的实例。

借用构造函数继承

通过在子类构造函数中调用父类的构造函数,可以使用call、apply方法来实现。

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']

相比于原型链而言,这种继承方法可以在子类构造函数中调用父类构造函数时传递参数。但是借用构造函数继承仍然有以下问题:

  • 只能够继承父类的实例的属性和方法,无法继承原型属性与方法
  • 无法复用,每个子类都有父类实例函数的副本,无法实现函数的复用。
组合继承

使用原型链方式继承原型属性与方法,使用借用构造函数方法来实现对于实例属性的继承。

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()

这种继承方式集合了原型链与借用构造函数方法的优势,既保证了函数方法的复用,同时也保证了每个实例都有自己的属性。但是,这种继承方式的局限在于:创建实例对象时,原型中会存在两份相同的属性、方法。
在这里插入图片描述

原型式继承

基本的思想是借助原型基于已有的对象来创建新的对象。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值