Life and freedom Ge Lin ——— Draw by Razzh

Javascript 继承总结

Sep 13 · 10 min

基于 JavaScript 高级程序设计中继承一章,总结一下继承方案。

#1、基本继承

首先来说说 Javascript 中实现继承的基本方式,如下代码所示:

function SuperType() {
  this.superOwner = 'razzh'
  this.brand = ['Ford', 'Cadillac']
}
 
SuperType.prototype.getSuperValue = function () {
  return this.superOwner
}
 
function SubType() {
  this.subOwner = 'ff'
}
 
SubType.prototype.subProperty = '我是子属性'
SubType.prototype = new SuperType()
 
var instance1 = new SubType()
var instance2 = new SubType()
instance1.brand.push('Benz')
// razzh
console.log(instance1.getSuperValue())
// undefined
console.log(instance1.subProperty)
// ['Ford', 'Cadillac', 'Benz']
console.log(instance1.brand)
// ['Ford', 'Cadillac', 'Benz']
console.log(instance2.brand)

实际上,基本继承只是重写了子构造函数的原型链以达到继承的目的。但是虽然继承的目的达到了,但是我们要知道如果 SubType 之前的原型上如果存在属性,通过上面的打印 instance1.subProperty 可以知道,我们并没有办法访问属性 subProperty ,再者,我们通过打印 instance2.brand 发现,我们并没有对 instance2brand 属性进行 push 操作,但是我们打印出来的 brand 中会多出这个 Benz 属性。
这主要跟 引用类型的地址 有关系:

SubType.prototype = new SuperType()
在原型上添加了 SuperType 上的属性, 实际上,后续的 instance1instance2 使用的都是同一引用

上述是对基本继承的描述,主要能看出两个缺点:

  • 子级构造函数原型链上面的属性会被覆盖
  • 若父级构造函数中存在引用类型的时候,各个实例对该引用的操作会影响其他实例。那么我们怎样才能做到继承 SuperType 上面的属性,在操作其引用类型的属性时,各实例之间不会相互影响呢?

#2、借用继承(经典继承)

这时,就需要介绍一下借用继承了:

function SuperType() {
  this.superOwner = arguments[0]
  this.brand = arguments[1]
}
 
SuperType.prototype.getSuperValue = function () {
  return this.superOwner
}
 
function SubType() {
  SuperType.apply(this, arguments)
  this.subOwner = 'ff'
}
 
SubType.prototype.subProperty = '我是子属性'
 
var instance1 = new SubType('razzh', ['Ford', 'Cadillac'])
var instance2 = new SubType('razzh', ['Ford', 'Cadillac'])
instance1.brand.push('Benz')
// throw error
// console.log(instance1.getSuperValue())
// 我是子属性
console.log(instance1.subProperty)
// ['Ford', 'Cadillac', 'Benz']
console.log(instance1.brand)
// ['Ford', 'Cadillac']
console.log(instance2.brand)

借用构造函数的原理是利用 使用 call或 apply 方法调用父构造函数,将父构造函数的 this 指向 SubType 构造函数实例。可以看到这时操作 instance1 后,打印 instance2 的值不会存在引用相互影响的情况,但是不能继承父构造函数的原型方法。

  • 优点:实现属性的继承
  • 缺点:不能继承父类原型上的方法

#3、组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承。是将基本继承和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。

function SuperType() {
  this.superOwner = 'razzh'
  this.brand = ['Ford', 'Cadillac']
}
 
SuperType.prototype.getSuperValue = function () {
  return this.superOwner
}
 
function SubType() {
  SuperType.call(this)
  this.subOwner = 'ff'
}
 
SubType.prototype.subProperty = '我是子属性'
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType // 修正constructor指向
 
var instance1 = new SubType()
var instance2 = new SubType()
instance1.brand.push('Benz')
// console.log(instance1.oldProperty) // throw error
console.log(instance1.getSuperValue()) // razzh
console.log(instance1.skill) // ['Ford', 'Cadillac', 'Benz']
console.log(instance2.skill) // ['Ford', 'Cadillac']

从打印结果来看,组合继承借用继承 的基础上又将父级构造函数的原型赋值给子类的构造函数,是其子拥有父级完整的属性,各个实例操作并不影响。

  • 优点:可以实现属性、以及方法的继承
  • 缺点:
    1. 调用了 2 次父类的构造函数,一次是 SuperType.call(),还有一次是在 new SuperType()
    2. 子类构造函数的原型被父类构造函数的原型覆盖,但实际上我们写子类的时候不会先在子类的原型上定义属性,而是先继承父类,再在子类的原型上定义方法

#4、寄生组合式继承

寄生组合式继承避免了组合继承中调用两次父类构造函数

 
function SuperType() {
  this.superOwner = arguments[0]
  this.brand = arguments[1]
}
 
SuperType.prototype.getBrand = function () {
  return this.brand
}
 
function SubType() {
  SuperType.apply(this, arguments)
  this.subOwner = 'ff'
}
 
SubType.prototype = Object.create(SuperType.prototype, {
  constructor: {
    value: SubType
  }
})
 
var instance = new SubType('razzh', ['Ford', 'Cadillac'])
console.log(instance.superOwner) // razzh
console.log(instance.getBrand()) // ['Ford', 'Cadillac']
  • 优点:这种方式的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了 SubType.prototype 上面创建不必要、多余的属性。

#5、ES6 继承

ES6 中 extends 的出现,让我们能够轻松完成 ES5 中复杂的继承模式:

class SuperType {
  constructor(name) {
    this.superProperty = name
  }
 
  getValue() {
    return this.superProperty
  }
}
 
class SubType extends SuperType {
  constructor(name) {
    super(name)
    this.subProperty = 'ff'
  }
}
 
var instance = new SubType('razzh')
// razzh
console.log(instance.getValue())

其采用了「extends」关键字,extends 可以指定类继承的父类,当子类使用 extends 继承父类,在子类如果需要写 constructor 函数,那么在函数中的顶部必须调用「super」关键字。

「super」方法的作用:

  1. 改变this指向,将父类的this指向子类
  2. 可向父类传递参数
  3. 访问父类原型上与子类同名的方法

前两条的实现原理实际上就是在子类中调用父类执行 call/apply 方法。 第三条来看下例:

class Parent {
  render() {
    console.log('父类的render函数')
  }
}
 
class Children extends Parent {
  constructor() {
    super()
  }
 
  init() {
    // 调用子类方法
    this.render()
    // 调用父类同名方法
    super.render()
  }
 
  render() {
    console.log('子类的render函数')
  }
}
 
new Children().init()
// 子类的render函数
// 父类的render函数

#Bebel 转译后的 class

在我们项目中的 class 类在打包的时候通常会被 Bebel 转译成 ES5 的代码:

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 },
  })
  Object.defineProperty(subClass, 'prototype', { writable: false })
  if (superClass) _setPrototypeOf(subClass, superClass)
}
 
function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf
    ? Object.setPrototypeOf.bind()
    : function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
  return _setPrototypeOf(o, p)
}

这是 Bebelextends 关键字转译后生成的代码,我们可以看到这实际上跟上面说的寄生组合式继承的实现方式是一样的,只不过 Bebel 的实现方案更细致一点,在后面使用了 Object.setPrototypeOf 方法将子类的 [[Prototype]] 也就是 __proto__ 修改成父类的 prototype

浙ICP备2024129591号-1
春秋(Live) - 张敬轩
--:-- / --:--
  1. 1春秋(Live)张敬轩
  2. 2不吐不快(live)张敬轩
  3. 3男孩最痛(live)张敬轩
  4. 4粤语残片(live)陈奕迅
  5. 5几分之几(live)卢广仲
  6. 6地球很危险古巨基
  7. 7樱花树下(live)张敬轩

春秋 (Live) - 张敬轩 (Hins Cheung)

词:林夕

曲:Edmond Tsang

那夜谁将酒喝掉

因此我讲得多了

然后你摇着我手拒绝我

动人像友情深了

我没权终止见面

只因你友善依然

仍用接近甜蜜那种字眼通电

没人应该 怨地怨天

得到这结局

难道怪罪神没有更伪善的祝福

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者 玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志

爱若能堪称伟大 再难挨照样开怀

如令你发现为你而活到失败

令人不安 我品性坏

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志 太没意思

若自觉这叫痛苦未免过份容易

我没有被你改写一生怎配有心事

我没有被你害过恨过

写成情史 变废纸

春秋只转载要事

如果爱你欠意义

这眼泪 无从安置

我没有运气放大自私的失意

更没有道理在这日

你得到真爱制造恨意

想心酸 还可以 想心底 留根刺

至少要见面上万次