【深入JavaScript】继承

本文深入探讨了JavaScript中的几种继承模式,包括原型链、盗用构造函数、组合继承、原型式继承、寄生式继承和寄生式组合继承。每种模式都有其优缺点,其中组合继承是最常用的,但存在效率问题。寄生式组合继承通过仅调用一次父类构造函数提高了效率,并保留了原型链,是引用类型继承的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承

原型链

  1. ECMA-262 把原型链定义为 ECMAScript 的主要继承方式
  2. 基本思想:通过原型继承多个引用类型的属性和方法
  3. 实现方式:子类的原型对象是父类的实例
  4. 存在问题:
    a) 引用类型的属性会在子类实例中共享
    b) 子类在实例化时,不能给父类的构造函数传参
  5. 评价:原型链存在的问题导致原型链基本不会被单独使用
function SuperType() {
	this.property = true
}

SuperType.propertype.getSuperValue = function () {
	return this.property
}

function SubType() {
	this.subproperty = false
}

// 继承 SuperType
subType.prototype = new SuperType()

subType.prototype.getSubvalue = function () {
	return this.subproperty
}

let instance = new SubType()
console.log(instance.getSuperValue)	// true
// 沿着原型链找到父类 SuperType 的原型对象上定义的 getSuperValue
// 沿着原型链找到在实例的原型(父类的实例)上定义的 property

盗用构造函数

  1. 为了解决原型链引用值共享的问题,开发社区开始流行一种叫做“盗用构造函数”的技术(也叫“对象伪装”或者“经典继承”)。

    盗用构造函数的继承方式还解决了原型链中不能够父类构造函数传参的问题。

  2. 基本思想:在子类的构造函数中调用父类的构造函数

  3. 实现:使用 apply () 或者 call () 以子类实例为上下文执行父类的构造函数

  4. 存在问题:

    a) 函数(方法)不能复用

    b) 子类不能调用定义在父类原型对象上的函数

  5. 评价:盗用构造函数存在的问题导致这种模式也不能单独使用

function SuperType(name) {
	this.colors = ['red', 'blue', 'green']
  this.name = name
}

function SubType() {
	// 继承 SuperType
	SuperType.call(this, 'zz')  // SuperType.apply(this)
}

let instance1= new SubType()
instance1.colors.push('black')
console.log(instance1.colors)	// ['red', 'blue', 'green', 'black']
conosle.log(instance1.name)

let instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green', 'black']

组合式继承

  1. 组合继承(也叫“伪经典继承”)综合了原型链和盗用构造函数。(组合式继承 = 原型链 + 盗用构造函数

  2. 基本思想:使用原型链继承 属性和方法,使用盗用构造函数继承实例属性

  3. 实现方式:子类的原型对象是父类的实例,使用 apply () 或 call () 以子类实例为上下文执行父类构造函数

  4. 存在问题:

  5. 评价:组合继承解决了原型链和盗用构造函数的不足,是 JavaScript 使用最多的继承模式

    ​ 并且组合继承还保留了 instanceof 操作符和 isPrototypeOf () 方法识别合成对象的能力

function SuperType(name) {
  // 实例属性
  this.name = name
  this.colors = ['red', 'green', 'blue']
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  // 盗用构造函数继承实例属性
  SuperType.call(this, name)
  
  this.age = age
}

// 原型链继承 属性和方法
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

let instance1 = new SubType('zz', 20)
instance1.colors.push('black')
console.log(instance1.colors)	// ['red', 'green', 'blue', 'black']
instance1.sayName()  // 'zz'
instance1.sayAge()  // 20

let instance2 = new SubType('yy', 19)
console.log(instance2.colors)  // ['red', 'green', 'blue']
instance1.sayName()  // 'yy'
instance2.sayName()  // 19

原型式继承

  1. 2006年,Douglas Crockford 提出了原型式继承的观点:即使不自定义类型也可以通过原型实现对象之间的信息共享

  2. 基本思想:【本质】利用原型链继承,对传入的对象执行一次浅拷贝

  3. 实现方式:

    封装一个继承函数,传入一个父类实例。

    a) 首先在这个继承函数中,声明一个临时的子类构造函数

    b) 然后进行原型链继承(子类的原型对象赋值为父类的实例)

    c) 最后返回一个通过临时的子类构造函数创建的子类实例

  4. 存在问题:由于原型式继承的是基于原型链继承的,因此,原型式继承同样会有原型式继承的问题

  5. 评价:

    a) 原型式继承是后来 ES5中的 Object.create() 方法的模拟

    b) 原型式继承非常适合不需要单独创建构造函数,但仍然需要实现对象共享数据的场景

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

let person = {
  name: 'zz',
  friends: ['Shelby', 'Court', 'Van']
}

let anotherPerson = object(person)
anotherPerson.name = 'yy'
anotherPerson.friends.push('Rob')

let yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')

console.log(person.friends)   // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']

寄生式继承

  1. 寄生式继承接近于原型式继承,也是 Crockford 首倡的一种模式

  2. 基本思想:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象(类似于寄生构造函数和工厂模式

  3. 实现方式:

    a) 通过原型式继承创建一个子类实例

    b) 定义子类实例的属性和方法(增强对象

    c) 返回这个子类实例

  4. 存在问题:和盗用构造函数一样,通过寄生式继承给子类实例定义方式时会导致函数难以重用

  5. 评价:和原型式继承一样,寄生式继承同样适合不在乎类型和构造函数的场景

function createAnother(original) {
  let clone = object(original)		// 通过原型式继承创建一个子类实例
  clone.sayHi = function () {			// 给子类实例定义实例属性和方法(增强对象)
    console.log('hi')
  }
  return clone		// 返回子类实例对象
}

let person = {
  name: 'zz',
  friends: ['Shelby', 'Court', 'van']
}

let anotherPerson = createAnother(person)
anotherPerson.sayHi()		// 'hi'

寄生式组合继承

  1. 组合继承其实也存在效率问题:父类构造函数始终会被调用两次(实现原型式继承时调用一次,实现盗用构造函数时又调用一次)。

    本质上,子类原型最终是要包含超类对象的所有实例属性,(重点在于)子类构造函数只要在执行时重写自己的原型就行了

    寄生式组合继承通过盗用构造函数继承属性,但是用混合式原型链继承方法。(寄生式组合继承 = 盗用构造函数 + 混合式原型链)

  2. 基本思想:仍然通过盗用构造函数继承实例属性。但是,在实现原型式继承时,不通过父类构造函数创建父类实例,而是通过寄生式继承获取父类原型的一个副本,再将这个父类原型的副本直接赋值给子类原型。

  3. 实现方式:

    a) 通过盗用构造函数继承实例属性

    b) 通过寄生式继承继承父类原型**(直接复制父类原型作为子类的原型)**

    ​ i) 通过原型式继承创建一个父类原型的副本

    ​ ii) 重写父类原型副本的 constructor 属性,让它指向子类的构造函数

    ​ iii) 将父类原型的副本赋值给子类原型(原型链继承)

  4. 存在问题

  5. 评价:

    ​ a) 寄生式组合继承只调用了一次父类的构造函数,避免了父类原型上不必要也用不到的属性,因此效率更高

    ​ b) 原型链仍然保持不变(子类原型是父类原型的副本,而创建父类原型副本时采用的是原型式继承创建父类实例),因此 instanceof 操作符和 isPrototypeOf () 方法仍然有效

    ​ c) 寄生式组合继承可以算是引用类型继承的最佳模式

function inheritPrototype(subType, superType) {		// 寄生式继承父类原型
  let prototype = object(superType.prototype)			// 组合式继承,获取父类原型副本
  prototype.construcor = subType		// 重写父类原型副本 constructor 指针
  subType.prototype = prototype			// 原型链继承,父类原型副本作为子类的原型
}

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = funtion () {
  console.log(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name)  // 盗用构造函数继承实例属性
  this.age = age
}

inheritPrototype(SubType, SuperType)   // 寄生式继承父类原型

subType.prototype.sayAge = function () {
  console.log(this.age)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zeekCheung

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值