JavaScript - 继承的几种实现方式

原型链继承

基于对象的原型链来实现,在 ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。可以总结为:当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype

原型链继承
javascript
1234567891011121314
function Parent() {
this.name = 'Parent'
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {}
Child.prototype = new Parent() // 继承自Parent
Child.prototype.constructor = Child
const child = new Child()
console.log(child.getName()) // "Parent"
缺点:
  • 包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱
  • 在创建子类型的实例时,不能向父类构造函数中传递参数

基于以上问题,在实践中很少会单独使用原型链。

构造函数继承

构造函数继承的思想主要是在子类构造函数中调用父类构造函数,使用 call 或 apply 实现

构造函数继承
javascript
123456789101112
function Parent(name) {
this.name = name
}
function Child(name, age) {
Parent.call(this, name) // 继承自Parent
this.age = age
}
const child = new Child('Child', 10)
console.log(child.name) // "Child"
console.log(child.age) // 10
优点:
  • 可以在子类型构造函数中向父类构造函数添加参数
  • 解决了原型链继承中引用类型属性共享的问题
缺点:
  • 只能继承父类的实例属性和方法,不能继承原型链上的属性和方法
  • 每次创建子类实例都会执行父类的构造函数,性能开销较大

基于以上问题,构造函数继承的技术也是很少单独使用的。

组合式继承

结合了原型链继承和构造函数继承的优点,既可以继承实例属性,也可以继承原型属性

组合继承
javascript
12345678910111213141516171819
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name, age) {
Parent.call(this, name) // 继承实例属性
this.age = age
}
Child.prototype = new Parent() // 继承原型属性
Child.prototype.constructor = Child
const child = new Child('Child', 10)
console.log(child.getName()) // "Child"
console.log(child.age) // 10
优点:
  • 组合继承结合了原型链继承和构造函数继承的优点,既可以继承父类的实例属性(包括引用类型的属性),也可以继承父类的原型方法
  • 通过构造函数调用,子类的实例属性不会共享父类的引用类型属性,从而避免了原型链继承中可能引发的属性共享问题
缺点:
  • 调用了两次父类的构造函数,导致基类的原型对象中增添了不必要的父类的实例对象中的所有属性

寄生式组合继承

寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。如下面的例子所示

寄生式组合继承
javascript
12345678910111213141516171819
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name, age) {
Parent.call(this, name) // 继承实例属性
this.age = age
}
Child.prototype = Object.create(Parent.prototype) // 继承原型属性
Child.prototype.constructor = Child
const child = new Child('Child', 10)
console.log(child.getName()) // "Child"
console.log(child.age) // 10
优点:
  • 解决了组合继承的冗余调用问题,性能更优
  • 继承了父类的实例属性和原型属性
缺点:
  • 使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用,实现稍微复杂一些

Class继承

ES6 引入了 class 关键字,提供了更直观的语法糖来实现继承

ES6 Class继承
javascript
1234567891011121314151617181920
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor(name, age) {
super(name) // 继承实例属性
this.age = age
}
}
const child = new Child('Child', 10)
console.log(child.getName()) // "Child"
console.log(child.age) // 10
优点:
  • 语法更加简洁和直观
  • 支持继承静态方法。
  • 更好的结构化代码,符合面向对象编程的习惯
优点:
  • 只是语法糖,本质上仍然基于原型链继承