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

相关推荐
Lsx-codeShare4 分钟前
一文读懂 Uniapp 小程序登录流程
前端·javascript·小程序·uni-app
一 乐8 分钟前
农产品电商|基于SprinBoot+vue的农产品电商系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot
地狱恶犬萨煤耶28 分钟前
JavaScript-小游戏-2048
javascript
爱心发电丶35 分钟前
基于UniappX开发电销APP,实现CRM后台控制APP自动拨号
javascript
地狱恶犬萨煤耶37 分钟前
JavaScript-实现函数方法-改变this指向call apply bind
javascript
地狱恶犬萨煤耶40 分钟前
JavaScript-小游戏-单词消消乐
javascript
tyro曹仓舒1 小时前
干了10年前端,才学会使用IntersectionObserver
前端·javascript
mine_mine2 小时前
油猴脚本拦截fetch和xhr请求,实现修改服务端接口功能
javascript
一 乐3 小时前
考公|考务考试|基于SprinBoot+vue的考公在线考试系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·课程设计
林太白3 小时前
跟着TRAE SOLO全链路看看项目部署服务器全流程吧
前端·javascript·后端