在JavaScript中,继承是面向对象编程的核心概念之一。随着语言的发展,其实现方式经历了从原型操作到语法糖的演进过程。本文将系统性地介绍五种主要的继承模式。
一、原型链继承
原型链继承通过直接将子类的原型指向父类实例来建立继承关系。
javascript
// 父类:用户
function User(username, password) {
this.username = username;
this.password = password;
this.permissions = ['read']; // 所有用户默认有读权限
}
// 子类:管理员
function Admin() {
// 这里无法向 User 传参!
// 我们希望传入 username 和 password,但做不到
}
// 原型链继承:关键一步
const userInstance = new User(); // 创建一个 User 的实例
Admin.prototype = userInstance; // 把 Admin 的原型换成这个实例
Admin.prototype.constructor = Admin;//constructor 是一个自动存在于原型对象上的属性,它指向"创建这个对象的函数"。
// 创建两个管理员
const admin1 = new Admin('admin1', '123456');
const admin2 = new Admin('admin2', '123456');
//无法传参数
console.log(admin1.username); // ❌ undefined
console.log(admin2.username); // ❌ undefined
// 我们想给 admin1 添加额外权限
admin1.permissions.push('write');
admin1.permissions.push('delete');
// 查看 admin2 的权限
console.log(admin2.permissions);
// ❌ 输出: ['read', 'write', 'delete']
// 但 admin2 什么都没做!它的权限被 admin1 改变了!
这种模式存在两个主要问题。首先,创建子类实例时无法向父类构造函数传递参数,导致admin1.username和admin2.username均为undefined。其次,由于所有实例共享同一个原型对象,当修改引用类型属性时会产生意外影响。例如,admin1.permissions.push('write')会使得admin2.permissions也包含'write'权限,因为两者访问的是同一数组实例。
缺点:
- 所有实例共享引用类型的属性(如数组)。
- 创建子类实例时,无法向父类构造函数传参。
二、构造函数借用与原型继承结合
为解决参数传递问题,可以在子类构造函数中使用call方法调用父类构造函数。
ini
function Admin(username, password, role) {
User.call(this, username, password);
this.role = 'admin';
}
这种方式使得每个实例都能获得独立的属性副本,解决了参数传递问题。同时通过Object.create方法继承原型:
ini
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
这样既保证了实例属性的独立性,又实现了原型方法的共享。实例属性属于特定对象,而原型上的方法被所有实例共享。
优点:
- 可以向父类传递参数。
- 每个实例都有独立的属性副本,不会相互影响。
三、组合继承
先回顾两种方式的优缺点
| 继承方式 | 优点 | 缺点 |
|---|---|---|
构造函数继承 Parent.call(this) |
✅ 每个实例都有独立的属性副本 ✅ 可以向父类传参 | ❌ 方法定义在构造函数里会重复创建 ❌ 无法共享方法(浪费内存) |
原型链继承 Child.prototype = new Parent() |
✅ 方法通过原型共享,节省内存 ✅ 支持继承父类原型上的方法 | ❌ 所有实例共享引用类型属性(如数组) ❌ 创建子类实例时无法向父类传参 |
用"构造函数继承"来解决"属性共享"问题(扬长避短)
用"原型链继承"来解决"方法共享"问题(发挥优势)
组合继承结合了构造函数借用和原型链继承的优点。它使用User.call(this, username, password)确保每个实例拥有独立的属性副本,避免引用类型属性的共享问题。同时通过Object.create(User.prototype)建立原型链,使子类能够继承父类原型上的方法。
ini
function Admin(username, password, role) {
User.call(this, username, password);
this.role = 'admin';
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
这种模式解决了前两种方式的主要缺陷,是JavaScript中常用的继承模式。每个实例都有独立的permissions数组,因此admin1.permissions的修改不会影响admin2.permissions。
js
function User(username, password) {
this.username = username;
this.password = password;
this.permissions = ['read']; // 每个实例都有自己的数组
}
function Admin(username, password, role) {
User.call(this, username, password); // 借用构造函数
this.role = 'admin';
}
// 继承原型
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
// 创建实例
const admin1 = new Admin('admin1', '123456');
const admin2 = new Admin('admin2', '123456');
console.log(admin1.username); // 'admin1'
console.log(admin2.username); // 'admin2'
admin1.permissions.push('write', 'delete');
console.log(admin1.permissions); // ['read', 'write', 'delete']
console.log(admin2.permissions); // ['read'] ✅ 不受影响
四、寄生组合式继承
先看经典的组合继承:
ini
function Child(name, age) {
Parent.call(this, name); // ✅ 第一次调用 Parent
this.age = age;
}
// ❌ 第二次调用 Parent!问题就在这里
Child.prototype = new Parent(); // new Parent() → 执行了 Parent 构造函数
Child.prototype.constructor = Child;
❌ 问题:父类构造函数被调用了 两次!
Parent.call(this, name)------ 为子类实例设置属性 ✅(必须的)new Parent()------ 为了继承原型,但这次调用是 多余的 ❌
这次多余的调用会导致:
- 浪费性能(执行了不必要的代码)。
- 如果
Parent构造函数中有副作用(如发请求、改全局变量),会被执行两次。 - 虽然
Child.prototype上的name和colors属性不会被实例使用(因为实例有自己的副本),但它们依然存在,有点"脏"。
🚫 目标:我们只想继承
Parent.prototype上的方法,但不想执行Parent构造函数!
ini
function Admin(username, password, role) {
User.call(this, username, password);
this.role = 'admin';
}
const prototype = Object.create(User.prototype);
prototype.constructor = Admin;
Admin.prototype = prototype;
关键在于Object.create(User.prototype)直接创建一个以User.prototype为原型的新对象,避免了执行User构造函数。这种方式只调用一次父类构造函数,既保证了属性的独立性,又实现了原型方法的共享,且没有多余的构造函数调用。
js
function User(username, password) {
this.username = username;
this.password = password;
this.permissions = ['read']; // 每个实例都有自己的数组
}
function Admin(username, password, role) {
User.call(this, username, password); // 借用构造函数
this.role = 'admin';
}
// 继承原型
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
// 创建实例
const admin1 = new Admin('admin1', '123456');
const admin2 = new Admin('admin2', '123456');
console.log(admin1.username); // 'admin1'
console.log(admin2.username); // 'admin2'
admin1.permissions.push('write', 'delete');
console.log(admin1.permissions); // ['read', 'write', 'delete']
console.log(admin2.permissions); // ['read'] ✅ 不受影响
五、ES6 Class语法
ES6引入了class关键字和extends语法,提供了更直观的继承语法。
scala
class User {
constructor(username, password) {
this.username = username;
this.password = password;
this.permissions = ['read'];
}
}
class Admin extends User {
constructor(username, password, role) {
super(username, password);
this.role = role || 'admin';
}
}
class语法本质上是寄生组合式继承的语法糖。extends关键字自动处理原型链的设置,super()调用父类构造函数。这种方式代码更简洁,语义更清晰,是当前推荐的继承实现方式。