JavaScript中的继承实现方式

在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.usernameadmin2.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;
❌ 问题:父类构造函数被调用了 两次
  1. Parent.call(this, name) ------ 为子类实例设置属性 ✅(必须的)
  2. new Parent() ------ 为了继承原型,但这次调用是 多余的

这次多余的调用会导致:

  • 浪费性能(执行了不必要的代码)。
  • 如果 Parent 构造函数中有副作用(如发请求、改全局变量),会被执行两次。
  • 虽然 Child.prototype 上的 namecolors 属性不会被实例使用(因为实例有自己的副本),但它们依然存在,有点"脏"。

🚫 目标:我们只想继承 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()调用父类构造函数。这种方式代码更简洁,语义更清晰,是当前推荐的继承实现方式。

相关推荐
我爱学习_zwj几秒前
动态HTTP服务器实战:解析请求与Mock数据
开发语言·前端·javascript
flashlight_hi5 分钟前
LeetCode 分类刷题:110. 平衡二叉树
javascript·算法·leetcode
Beginner x_u6 分钟前
Vue 事件机制全面解析:原生事件、自定义事件与 DOM 冒泡完全讲透
前端·javascript·vue.js·dom
郑州光合科技余经理37 分钟前
实战分享:如何构建东南亚高并发跑腿配送系统
java·开发语言·javascript·spring cloud·uni-app·c#·php
June bug39 分钟前
【Vue】EACCES: permission denied 错误
前端·javascript·vue.js
一只小阿乐1 小时前
react 中的组件性能优化
前端·javascript·react.js·react组件性能优化
xiaoxue..1 小时前
二叉树深度解析:从基础结构到实战应用
javascript·数据结构·面试
月巴月巴白勺合鸟月半1 小时前
一个医学编码的服务
服务器·前端·javascript
方也_arkling1 小时前
【JS】定时器的使用(点击开始计时,再次点击停止计时)
开发语言·前端·javascript
乆夨(jiuze)1 小时前
不是所有的链式调用,都是Promise函数,Promise 规范及其衍生的 Promise/A+ 规范
前端·javascript·vue.js