一、继承的本质与核心分类
继承是面向对象编程的核心概念,通过原型链实现属性和方法的复用。在JavaScript中,常见的继承方式可分为原型链继承、构造函数继承、组合继承等,每种方式各有优缺点。
二、经典继承方式手写实现与缺点
1. 原型链继承(最基础方式)
// 父类
function Parent() {
this.name = 'parent';
this.hobbies = ['reading'];
}
Parent.prototype.getName = function() {
return this.name;
};
// 原型链继承
function Child() {}
Child.prototype = new Parent(); // 关键:将子类原型指向父类实例
Child.prototype.constructor = Child; // 修复constructor指向
// 测试
const child1 = new Child();
const child2 = new Child();
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading', 'coding'] 共享引用类型属性
缺点:
- 引用类型属性共享:子类实例共享父类原型中的引用类型属性(如数组、对象),一个实例修改会影响所有实例;
- 无法向父类构造函数传参:无法在创建子类实例时向父类传递参数(如
new Child('参数')
无效); - 原型污染风险:直接修改
Child.prototype
可能意外覆盖父类原型方法。
2. 构造函数继承(借用构造函数)
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
// 构造函数继承
function Child(name, age) {
Parent.call(this, name); // 关键:在子类中调用父类构造函数
this.age = age;
}
// 测试
const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading'] 引用类型属性独立
缺点:
- 无法继承父类原型方法:子类只能继承父类构造函数中的属性,无法继承
Parent.prototype
中的方法(如getName
); - 代码复用性差:父类原型方法需在子类中重复定义,违反DRY原则;
- 构造函数调用冗余:每次创建子类实例都需重新执行父类构造函数逻辑。
3. 组合继承(原型链+构造函数)
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
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 child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading'] 引用类型独立
console.log(child1.getName()); // '小明' 方法继承正常
缺点:
- 父类构造函数执行两次:
- 第一次:
Child.prototype = new Parent()
时创建父类实例; - 第二次:
new Child()
时通过Parent.call(this)
执行父类构造函数; - 导致父类属性在原型和实例中重复存在,浪费内存。
- 第一次:
- 原型链污染风险:若父类构造函数有副作用(如修改全局变量),会被执行两次。
4. 寄生组合继承(优化组合继承)
function Parent(name) {
this.name = name;
this.hobbies = ['reading'];
}
Parent.prototype.getName = function() {
return this.name;
};
// 寄生组合继承(最优方式)
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 优化原型链:避免创建父类实例
function createPrototype(parent) {
const proto = Object.create(parent.prototype); // 核心:用Object.create创建原型
proto.constructor = Child;
return proto;
}
Child.prototype = createPrototype(Parent);
// 测试
const child1 = new Child('小明', 18);
console.log(Child.prototype instanceof Parent); // true 原型链正确
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // true 避免重复执行构造函数
缺点:
- ES5语法复杂度:需手动实现
createPrototype
函数,代码量增加; - 浏览器兼容性:
Object.create
在IE9+才支持,低版本需polyfill。
5. ES6类继承(class语法)
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['reading'];
}
getName() {
return this.name;
}
}
// ES6类继承
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调用super()
this.age = age;
}
}
// 测试
const child = new Child('小明', 18);
缺点:
- 底层仍是组合继承:本质是寄生组合继承的语法糖,仍存在构造函数执行顺序限制(必须先调用
super()
); - 无法完全避开原型链:若父类原型方法修改了
this
指向,子类可能受影响。
三、各继承方式缺点对比表
继承方式 | 核心缺点 | 内存效率 | 代码复杂度 |
---|---|---|---|
原型链继承 | 引用类型共享、无法传参、原型污染 | 低 | 低 |
构造函数继承 | 无法继承原型方法、代码重复 | 中 | 中 |
组合继承 | 父类构造函数执行两次、内存浪费 | 中 | 中 |
寄生组合继承 | ES5语法复杂、低版本浏览器兼容问题 | 高 | 高 |
ES6类继承 | 依赖super()执行顺序、底层仍有组合继承痕迹 | 高 | 低 |
四、问题
1. 问:为什么组合继承会执行两次父类构造函数?
- 答:
组合继承通过Child.prototype = new Parent()
创建父类实例(第一次执行构造函数),再通过Parent.call(this)
在子类构造函数中第二次执行。这会导致父类属性在原型和实例中重复存在,浪费内存。寄生组合继承通过Object.create(parent.prototype)
避免了第一次构造函数执行,是更优的方案。
2. 问:ES6类继承中的super有什么作用?
- 答:
super()
用于调用父类构造函数,必须在子类constructor
中先于this
使用;super.方法名
用于调用父类原型方法,解决了ES5中Parent.prototype.method.call(this)
的繁琐写法;- 本质是寄生组合继承的语法糖,底层仍通过原型链实现继承。
3. 问:如何实现一个寄生组合继承?请手写核心代码。
- 答:
function inherit(child, parent) { // 1. 创建父类原型的副本 const proto = Object.create(parent.prototype); // 2. 设置子类原型的constructor proto.constructor = child; // 3. 将子类原型指向副本 child.prototype = proto; } // 使用示例 function Parent() { /* ... */ } function Child() { /* ... */ } inherit(Child, Parent);