JavaScript 继承机制全解析:从原型链到寄生组合式

作者:半夏时间:2026-10-23 23:31:34浏览:376

本文深入解析 JavaScript 的六种继承方式。从基础的构造函数与原型模式出发,详解原型链、构造函数、组合、原型式、寄生式及寄生组合式继承的原理与优缺点。重点剖析寄生组合式继承为何被视为最理想的实现方案,帮助开发者理解 ES6 Class 背后的底层逻辑。

前言

提及JavaScript继承,你脑海中浮现的是什么?面试中常问的继承方式及其优缺点……JS继承曾是令人头疼的难题,我看了忘、忘了看、看了又忘,天哪,都9012年了,面试官为何还不放过我。

玩笑归玩笑,让我们回到正题,深入探讨 JavaScript 继承这一核心概念。

JavaScript 中类的模拟方式

JS 的“类”

与 Java、PHP 等传统面向对象语言不同,JavaScript 原生中并不存在“类”的概念。那么,它是如何模拟出类的行为呢?

  1. 构造函数方式
  2. 原型方式
  3. 混合方式

构造函数方式


Function Foo (name) {
 this.name = name
 this.like = function () {
 console.log(`like${this.name}`)
 }
}
let foo = new Foo('bibidong')

上述代码展示了如何通过构造函数定义类。其本质与普通函数无异,但为了区分,惯例是将函数名的首字母大写。

缺点:这种方法无法共享类的方法,每个实例都会创建独立的方法副本。

原型方式


function Foo (name) {}
Foo.prototype.color = 'red'
Foo.prototype.queue = [1,2,3]
let foo1 = new Foo()
let foo2 = new Foo()

foo1.queue.push(4)
console.log(foo1) // [1, 2, 3, 4]
console.log(foo2) // [1, 2, 3, 4]

通过原型方式,我们将属性和方法直接定义在构造函数的原型对象上。这样,实例可以共享这些属性和方法,从而解决了构造函数方式中方法无法共享的问题。

缺点:观察代码可知,当我们修改 foo1 的数据时,foo2 的 queue 属性也随之改变。这是原型方式最大的缺陷:引用类型的属性会被其他实例共享并修改。此外,这种方式下也无法向构造函数传递参数。

混合方式


function Foo (name) { // 属性定义在构造函数里面
 this.name = name
 this.color = 'red'
 this.queue = [1,2,3]
}
Foo.prototype.like = function () { // 方法定义在原型上
 console.log(`like${this.name}`)
}
let foo1 = new Foo()
let foo2 = new Foo()

所谓混合模式,即结合上述两种方式:在构造函数中定义属性,在原型对象上定义共享方法。这样既支持传参,又避免了原型模式中引用类型共享的问题。

小结:JavaScript 的类能力是通过模拟实现的,可以通过构造函数方式或原型方式来定义。混合模式聚合了前两者的优点。此外,Object.create() 和 ES6 的 class 语法也可用于创建对象和定义类。

JavaScript 常见继承方式详解

常见的继承方式

原型链继承

一、原型链继承

基于原型链的查找机制,我们将父类的实例作为子类的原型,这种继承方式称为原型链继承。


function Parent () {
 this.color = 'red'
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log('')
}

function Child () { }
Child.prototype = new Parent() // constructor指针变了 指向了Parent
Child.prototype.constructor = Child // 手动修复

let child = new Child()

Child.prototype 相当于父类 Parent 的实例,父类的实例属性被挂载到了子类的原型对象上。以 color 属性为例,其结构大致如下:


Child.prototype.color = 'red'

这样,父类的实例属性都被共享了。打印 child 可以看到,它没有自己的实例属性,访问的是其原型对象上的属性。

JavaScript 中类的模拟方式

我们创建两个实例 child1 和 child2。


let child1 = new Child()
let child2 = new Child()
child1.color = 'bulr'
console.log(child1)
console.log(child2)

JavaScript 中类的模拟方式

当我们修改 child1 的 color 属性时,child2 并未受到影响。这并非因为其他实例拥有独立的 color 属性,而是因为该属性直接添加到了 child1 实例上,原型上的 color 并未改变,因此其他实例不受影响。从打印结果可以清晰看到这一点。那么,如果修改的属性是引用类型呢?


child1.queue = [1,2,3,'我被修改了'] // 重新赋值
child1.like = function () {console.log('like方法被我修改了')}
console.log(child1)
console.log(child2)

JavaScript 中类的模拟方式

我们重写了引用类型的 queue 属性和 like 方法,这与修改 color 属性完全一致,它们都直接添加到了 child1 的实例属性上。从打印结果能看到这两个属性已添加到了 child1 上,而 child2 不受影响。再看下面这个例子。


child1.queue.push('add push') // 这次没有重新赋值
console.log(child1)
console.log(child2)

JavaScript 中类的模拟方式

如果进行重新赋值,属性会添加到实例上,与原型上的同名属性无关,因此不会影响原型。但如果这次采用 push 方法,没有开辟新空间,修改的就是原型对象。此时 child2 的 queue 属性发生了变化,子类 Child 原型上的 queue 属性被实例修改,这肯定会影响所有实例。

缺点

  • 子类的实例会共享父类构造函数中引用类型的属性。
  • 创建子类实例时无法传递参数。

构造函数式继承

二、构造函数式继承

这种方式相当于拷贝父类的实例属性给子类,增强了子类构造函数的能力。


function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name) // 核心代码
}

let child = new Child(1)

JavaScript 中类的模拟方式

打印 child 可以看到,子类拥有父类的实例属性和方法,但 child 的 __proto__ 上并没有指向父类的原型对象。这解决了原型链继承的两个问题:子类实例的各个属性相互独立,且支持传参。

缺点

  • 子类无法继承父类原型上的方法和属性。
  • 在构造函数中定义的方法,每次创建实例时都会重新创建一遍,造成资源浪费。

组合继承

三、组合继承

人如其名,组合继承是将上述两种继承方式进行组合。


function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name)
}

Child.prototype = new Parent()
Child.prototype.constructor = Child  // 修复constructor指针
let child = new Child('')

接下来我们进行一些操作,看看组合后能否发扬原型链继承和构造函数继承的优点。


let child1 = new Child('bibidong')
let child2 = new Child('huliena')
child1.queue.push('add push')
console.log(child1)
console.log(child2)

JavaScript 中类的模拟方式

我们更新 child1 的引用属性,发现 child2 实例未受影响,且原型上的 like 方法依然存在。不错,组合继承确实将二者的优点发扬光大,解决了前两者的缺点。在组合模式下,通常在构造函数上定义实例属性,在原型对象上定义共享方法。通过原型链继承方法,让子类继承父类构造函数原型上的方法;通过构造函数继承,子类得以继承构造函数的实例属性。这是一种功能上较完美的继承方式。

缺点:父类构造函数被调用了两次。第一次调用后,子类的原型上拥有了父类的实例属性;第二次 call 调用复制了一份父类的实例属性作为子类 Child 的实例属性,那么子类原型上的同名属性就被覆盖了。虽然被覆盖后功能上没什么大问题,但这份多余的同名属性一直存在于子类原型上。如果我们删除实例上的这个属性,实际上还能访问到它原型上的属性。


Child.prototype = new Parent() // 第一次构建原型链
Parent.call(this, name) // 第二次new操作符内部通过call也执行了一次父类构造函数

原型式与寄生式继承

四、原型式继承

将一个对象作为基础,经过处理得到一个新对象,这个新对象会将原来那个对象作为原型,这种继承方式称为原型式继承。一句话总结:将传入的对象作为要创建的新对象的原型。

先写下这个具有处理能力的函数。


function prodObject (obj) {
 function F (){
  
 }
 F.prototype = obj
 return new F() // 返回一个实例对象
}

这也是Object.create()的实现原理,所以用Object.create直接替换prodObject函数是ok的

这也是 Object.create() 的实现原理,因此用 Object.create 直接替换 prodObject 函数是可行的。


let base = {
 color: 'red',
 queue: [1, 2, 3]
}
let child1 = prodObject(base)
let child2 = prodObject(base)
console.log(child1)
console.log(child2)

JavaScript 中类的模拟方式

原型式继承基于 prototype,与原型链继承类似。在这种继承方式下,实例没有自己的属性值,访问到的也是原型上的属性。

缺点:与原型链继承相同。

五、寄生式继承

原型式继承的升级版。寄生继承封装了一个函数,在内部增强了原型式继承产生的对象。


function greaterObject (obj) {
 let clone = prodObject(obj)
 clone.queue = [1, 2, 3]
 clone.like = function () {}
 return clone
}
let parent = {
 name: 'bibidong',
 color: ['red', 'bule', 'black']
}
let child = greaterObject(parent)

打印 child 可以看到,其缺点也很明显:寄生式继承增强了对象,却无法避免原型链继承的问题。

缺点

  • 拥有原型链继承的所有缺点。
  • 此外,内部的函数无法复用。

寄生组合式继承

六、寄生组合式继承

大招来了,寄生组合式继承闪亮登场!

前面提到,组合继承的问题在于会调用两次父类构造函数,导致子类原型上产生多余的同名属性。即 Child.prototype = new Parent() 这行代码。那么,这行代码该如何改造呢?

我们的目的是让父类的实例属性不出现在子类原型上。如果让 Child.prototype = Parent.prototype,这样就能保证子类只挂载父类原型上的方法,实例属性自然就没有了。代码如下,看起来简直太妙了。


function Parent (name) {
 this.name = name
 this.queue = [1,2,3]
}
Parent.prototype.like = function () {
 console.log(`like${this.name}`)
}

function Child (name) {
 Parent.call(this, name)
}

Child.prototype = Parent.prototype // 只改写了这一行
Child.prototype.constructor = Child
let child = new Child('')

但仔细一想,如果 Child.prototype 直接指向 Parent.prototype,一旦改变 Child.prototype,岂不是直接影响到了父类?举个栗子:


Child.prototype.addByChild = function () {}
Parent.prototype.hasOwnProperty('addByChild') // true

addByChild 方法也被加到了父类的原型上,所以这种方法不够优雅。同样还是那一行代码,直接访问 Parent.prototype 存在问题。那我们可以产生一个以 Parent.prototype 作为原型的新对象,这不就是上面原型式继承中的处理函数 prodObject 吗?


Child.prototype = Object.create(Parent.prototype) // 改为这样

这样就解决了所有问题。我们担心改写 Child.prototype 会影响父类,通过 Object.create 返回的实例对象,我们将 Child.prototype 间接指向 Parent.prototype。当再增加 addByChild 方法时,属性就和父类无关了。

寄生组合式继承也被认为是最完美的继承方式,最推荐使用。

继承方式总结

总结

JavaScript 的继承方式主要有这六种。ES6 的继承是一个语法糖,其本质也是基于寄生组合式。这六种继承方式中,原型链继承和构造函数继承最为基础和经典;组合继承聚合了它们二者的能力,但在某些情况下会造成错误;原型式继承与原型链相似;寄生式继承是在原型式继承基础上变化而来,它增强了原型式继承的能力。最后的寄生组合式继承解决了组合继承的问题,是一种最为理想的继承方式。

到此这篇关于 JavaScript 继承机制的文章就介绍到这了,更多相关内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章