JavaScript 常见继承方式详解
JavaScript 作为一门基于原型的语言,其继承机制与传统的基于类的语言有所不同。本文将详细讲解 JavaScript 中六种常见的继承方式,包括它们的实现原理、优缺点以及适用场景。
1. 原型链继承
原型链继承是 JavaScript 中最基本的继承方式,它利用原型链的特性实现继承。
实现原理
javascript
function Parent() {
this.name = 'parent';
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {
this.type = 'child';
}
// 关键步骤:将 Child 的原型指向 Parent 的实例
Child.prototype = new Parent();
const child1 = new Child();
console.log(child1.getName()); // 输出: parent
底层机制
- 当访问一个对象的属性时,JavaScript 引擎会首先在对象自身查找
- 如果没有找到,则会沿着
__proto__
指针向上查找 - 通过将
Child.prototype
设置为Parent
的实例,建立了原型链 - 这样
Child
实例就可以访问Parent
实例及其原型上的属性和方法
优缺点
优点:
- 简单易实现
- 父类新增原型方法/属性,子类都能访问到
缺点:
- 所有子类实例共享父类实例的属性,如果属性是引用类型,一个子类实例修改会影响其他实例
- 创建子类实例时,无法向父类构造函数传参
2. 构造函数继承(借助 call)
构造函数继承通过在子类构造函数中调用父类构造函数来实现继承。
实现原理
javascript
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
// 关键步骤:在子类构造函数中调用父类构造函数
Parent.call(this, name);
this.age = age;
}
const child1 = new Child('Tom', 18);
console.log(child1.name); // 输出: Tom
console.log(child1.getName); // 输出: undefined
底层机制
- 使用
call
或apply
方法在子类构造函数中调用父类构造函数 - 这样父类构造函数中的
this
指向的是子类实例 - 父类实例属性会被复制到子类实例上
- 但父类原型上的方法不会被继承
优缺点
优点:
- 避免了引用类型属性被所有实例共享
- 可以在子类中向父类传递参数
缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法
- 不能继承父类原型上的属性和方法
3. 组合继承
组合继承结合了原型链继承和构造函数继承的优点,是 JavaScript 中最常用的继承模式。
实现原理
javascript
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
// 第二次调用 Parent()
Parent.call(this, name);
this.age = age;
}
// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 输出: Tom
底层机制
- 使用构造函数继承父类的实例属性
- 使用原型链继承父类原型上的方法和属性
- 通过这种方式,既可以让每个实例拥有自己的属性,又可以共享方法
优缺点
优点:
- 融合了原型链继承和构造函数继承的优点
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例和子类原型各有一份)
4. 原型式继承
原型式继承基于已有对象创建新对象,是 ES5 Object.create()
的规范化实现。
实现原理
javascript
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}
const parent = {
name: 'parent',
colors: ['red', 'blue', 'green'],
getName: function() {
return this.name;
}
};
const child1 = createObj(parent);
console.log(child1.getName()); // 输出: parent
底层机制
- 创建一个临时构造函数
- 将传入的对象作为这个构造函数的原型
- 返回这个临时类型的新实例
- 本质上是对传入对象进行了一次浅复制
优缺点
优点:
- 简单,不需要单独创建构造函数
- 适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场景
缺点:
- 包含引用类型的属性值始终会共享相应的值
- 无法实现代码复用(新实例属性都是后面添加的)
5. 寄生式继承
寄生式继承是在原型式继承的基础上增强对象,返回新对象。
实现原理
javascript
function createAnother(original) {
const clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
const parent = {
name: 'parent',
colors: ['red', 'blue', 'green'],
getName: function() {
return this.name;
}
};
const child1 = createAnother(parent);
child1.sayHi(); // 输出: hi
底层机制
- 创建一个基于原对象的对象
- 增强这个对象(添加新方法/属性)
- 返回增强后的对象
优缺点
优点:
- 可以在不修改原对象的情况下增强对象
- 适合主要关注对象而不是自定义类型和构造函数的场景
缺点:
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率
- 同原型式继承一样,引用类型属性会被共享
6. 寄生组合式继承
寄生组合式继承是目前最理想的继承方式,它通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
实现原理
javascript
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键步骤
inheritPrototype(Child, Parent);
const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 输出: Tom
底层机制
- 使用构造函数继承父类的实例属性
- 使用寄生式继承来继承父类的原型
- 通过创建一个父类原型的副本并将其赋值给子类原型来实现
- 避免了组合继承中调用两次父类构造函数的问题
优缺点
优点:
- 只调用一次父类构造函数
- 避免了在子类原型上创建不必要的、多余的属性
- 原型链保持不变
- 能够正常使用 instanceof 和 isPrototypeOf
缺点:
- 实现相对复杂
总结
JavaScript 的继承方式各有特点:
- 原型链继承:简单但引用属性共享
- 构造函数继承:解决属性共享问题但无法继承原型方法
- 组合继承:常用但会调用两次父类构造函数
- 原型式继承:适合简单对象继承
- 寄生式继承:增强对象但不适合代码复用
- 寄生组合式继承:最理想的继承范式
在实际开发中,寄生组合式继承是最为推荐的方式,它避免了组合继承的缺点,同时保持了继承的完整性和高效性。ES6 的 class 继承语法在底层也是使用类似寄生组合式继承的方式实现的。