JS继承方式详解

JavaScript 继承基于原型链 实现,不存在类继承的原生语法(ES6 class 是语法糖,底层仍为原型继承),常见继承方式按演进逻辑可分为以下 6 种,各有优劣与适用场景:

一、原型链继承(最基础的继承方式)

核心原理

将父类的实例作为子类的原型(SubType.prototype = new SuperType()),子类实例通过原型链向上查找父类的属性和方法,实现继承。

代码示例

javascript

运行

js 复制代码
// 父类
function Animal(name) {
  this.name = name; // 实例属性
  this.colors = ['black', 'white']; // 引用类型实例属性
}
Animal.prototype.sayName = function() { // 原型方法
  console.log('动物名称:', this.name);
};

// 子类
function Dog() {}
// 核心:将父类实例赋值给子类原型
Dog.prototype = new Animal('小狗');
Dog.prototype.constructor = Dog; // 修复构造函数指向

// 测试
const dog1 = new Dog();
const dog2 = new Dog();
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white', 'brown'](引用类型属性被共享)
dog1.sayName(); // 动物名称:小狗

优点与缺点

  • 优点:实现简单,子类可继承父类原型上的所有方法;

  • 缺点

    1. 父类的引用类型实例属性会被所有子类实例共享(一个实例修改会影响其他实例);
    2. 无法向父类构造函数传递参数(子类实例创建时,无法自定义父类实例属性)。

二、构造函数继承(借用父类构造函数)

核心原理

在子类构造函数中,通过 call()/apply() 调用父类构造函数,将父类的实例属性绑定到子类实例上,实现实例属性的继承。

代码示例

javascript

运行

js 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
  this.sayName = function() {
    console.log('动物名称:', this.name);
  };
}

// 子类
function Dog(name, breed) {
  // 核心:借用父类构造函数,传递参数
  Animal.call(this, name);
  this.breed = breed; // 子类自有属性
}

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型属性不共享)
dog1.sayName(); // 动物名称:旺财
console.log(dog1.breed); // 中华田园犬

优点与缺点

  • 优点

    1. 解决了原型链继承中引用类型属性共享的问题;
    2. 可以向父类构造函数传递参数;
  • 缺点

    1. 只能继承父类的实例属性和方法,无法继承父类原型上的方法(每个子类实例都会复制一份父类方法,浪费内存);
    2. 子类实例无法共享父类方法,违背原型链的设计初衷。

三、组合继承(原型链 + 构造函数,最常用)

核心原理

结合原型链继承和构造函数继承的优点:

  1. 原型链继承继承父类原型上的方法(实现方法共享);
  2. 构造函数继承继承父类的实例属性(避免引用类型共享,支持传参)。

代码示例

javascript

运行

js 复制代码
// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}
Animal.prototype.sayName = function() {
  console.log('动物名称:', this.name);
};

// 子类
function Dog(name, breed) {
  // 构造函数继承:继承实例属性,传参
  Animal.call(this, name);
  this.breed = breed;
}
// 原型链继承:继承原型方法,实现方法共享
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复构造函数指向
// 子类原型方法
Dog.prototype.sayBreed = function() {
  console.log('犬种:', this.breed);
};

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 动物名称:旺财(继承父类原型方法)
dog1.sayBreed(); // 犬种:中华田园犬(子类自有方法)
console.log(dog1 instanceof Animal); // true( instanceof 检测正常)

优点与缺点

  • 优点

    1. 兼顾了原型链继承和构造函数继承的优点,既实现了方法共享,又避免了引用类型属性共享;
    2. 支持向父类传参,instanceof 检测正常;
  • 缺点 :父类构造函数被调用了两次(一次是创建子类原型时 new Animal(),一次是子类构造函数中 Animal.call(this)),导致子类原型上存在多余的父类实例属性(虽不影响使用,但造成内存冗余)。

四、原型式继承(基于已有对象创建新对象)

核心原理

通过 Object.create()(或手动封装的原型方法),以一个已有对象为原型,创建新的对象,实现对已有对象属性和方法的继承。

代码示例

javascript

运行

js 复制代码
// 已有对象(作为原型)
const animal = {
  name: '动物',
  colors: ['black', 'white'],
  sayName: function() {
    console.log('动物名称:', this.name);
  }
};

// 核心:用 Object.create 创建新对象,继承 animal
const dog = Object.create(animal);
dog.name = '旺财'; // 重写实例属性
dog.breed = '中华田园犬'; // 新增自有属性

// 测试
const cat = Object.create(animal);
dog.colors.push('brown');
console.log(dog.colors); // ['black', 'white', 'brown']
console.log(cat.colors); // ['black', 'white', 'brown'](引用类型属性共享)
dog.sayName(); // 动物名称:旺财
console.log(dog.breed); // 中华田园犬

优点与缺点

  • 优点:无需定义构造函数,实现简单,适合快速创建基于已有对象的新对象;

  • 缺点

    1. 引用类型属性会被所有新对象共享(与原型链继承一致);
    2. 无法向父对象传递参数,只能在创建新对象后手动修改属性。

五、寄生式继承(原型式继承的增强版)

核心原理

在原型式继承的基础上,封装一个创建对象的函数,在函数内部为新对象添加自有属性和方法,增强新对象的功能,最后返回新对象。

代码示例

javascript

运行

js 复制代码
// 封装创建继承对象的函数(寄生函数)
function createAnimal(proto, name, breed) {
  // 原型式继承:创建新对象
  const obj = Object.create(proto);
  // 增强新对象:添加自有属性和方法
  obj.name = name;
  obj.breed = breed;
  obj.sayBreed = function() {
    console.log('犬种/品种:', this.breed);
  };
  return obj;
}

// 原型对象
const animal = {
  colors: ['black', 'white'],
  sayName: function() {
    console.log('名称:', this.name);
  }
};

// 测试
const dog = createAnimal(animal, '旺财', '中华田园犬');
const cat = createAnimal(animal, '咪咪', '橘猫');
dog.colors.push('brown');
console.log(dog.colors); // ['black', 'white', 'brown']
console.log(cat.colors); // ['black', 'white', 'brown'](引用类型共享)
dog.sayName(); // 名称:旺财
dog.sayBreed(); // 犬种/品种:中华田园犬

优点与缺点

  • 优点:无需定义构造函数,可灵活增强新对象的功能,实现简单;

  • 缺点

    1. 引用类型属性共享问题依然存在;
    2. 每个新对象的自有方法都是独立的(无法共享),浪费内存;
    3. 无法实现方法的复用,类似构造函数继承的缺点。

六、寄生组合式继承(完美继承方案)

核心原理

结合组合继承和寄生式继承的优点,解决组合继承中父类构造函数被调用两次的问题:

  1. 寄生式继承继承父类的原型(仅继承原型方法,不调用父类构造函数);
  2. 构造函数继承继承父类的实例属性(支持传参,避免引用类型共享)。

代码示例

javascript

运行

js 复制代码
// 寄生函数:继承父类原型,不调用父类构造函数
function inheritPrototype(SubType, SuperType) {
  // 创建父类原型的副本(避免直接修改父类原型)
  const prototype = Object.create(SuperType.prototype);
  prototype.constructor = SubType; // 修复构造函数指向
  SubType.prototype = prototype; // 将副本赋值给子类原型
}

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}
Animal.prototype.sayName = function() {
  console.log('名称:', this.name);
};

// 子类
function Dog(name, breed) {
  // 构造函数继承:继承实例属性,传参(仅调用一次父类构造函数)
  Animal.call(this, name);
  this.breed = breed;
}

// 核心:寄生式继承父类原型
inheritPrototype(Dog, Animal);

// 子类原型方法
Dog.prototype.sayBreed = function() {
  console.log('犬种:', this.breed);
};

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 名称:旺财
dog1.sayBreed(); // 犬种:中华田园犬
console.log(dog1 instanceof Animal); // true
console.log(Dog.prototype.constructor === Dog); // true(构造函数指向正确)

优点与缺点

  • 优点

    1. 父类构造函数仅被调用一次,避免了内存冗余;
    2. 实现了方法共享,避免了引用类型属性共享;
    3. 支持向父类传参,instanceof 检测和构造函数指向均正常;
    4. 是 JavaScript 继承的 "完美方案",ES6 class extends 底层基于此实现。
  • 缺点:实现相对复杂(需封装寄生函数),但可复用该函数。

七、ES6 Class 继承(语法糖)

核心原理

通过 class 定义类,extends 关键字实现继承,super() 调用父类构造函数,底层仍是寄生组合式继承,只是语法更简洁、更接近传统类继承。

代码示例

javascript

运行

js 复制代码
// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ['black', 'white'];
  }

  sayName() {
    console.log('名称:', this.name);
  }
}

// 子类:extends 实现继承
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须先调用 super(),才能使用 this
    this.breed = breed;
  }

  sayBreed() {
    console.log('犬种:', this.breed);
  }
}

// 测试
const dog1 = new Dog('旺财', '中华田园犬');
const dog2 = new Dog('小白', '萨摩耶');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'](引用类型不共享)
dog1.sayName(); // 名称:旺财
dog1.sayBreed(); // 犬种:中华田园犬
console.log(dog1 instanceof Animal); // true

优点与缺点

  • 优点 :语法简洁直观,符合面向对象编程习惯,易于理解和维护,支持静态方法继承(static 关键字);
  • 缺点:本质是语法糖,底层仍依赖原型链,新手可能忽略原型继承的本质。

八、各类继承方式对比与选型建议

继承方式 核心优点 核心缺点 适用场景
原型链继承 实现简单,方法共享 引用类型共享,无法传参 简单场景,无需传参,不关心引用类型共享
构造函数继承 支持传参,引用类型不共享 无法继承原型方法,方法冗余 仅需继承实例属性,无需共享方法
组合继承 方法共享,支持传参,功能完善 父类构造函数调用两次 常规业务场景,兼容性要求高
原型式继承 无需构造函数,快速创建对象 引用类型共享,无法传参 基于已有对象快速创建新对象
寄生式继承 灵活增强对象功能 方法冗余,引用类型共享 快速创建并增强新对象,简单场景
寄生组合式继承 完美解决所有缺陷,性能最优 实现复杂 追求性能和严谨性的场景,框架开发
ES6 Class 继承 语法简洁,符合 OOP 习惯 底层仍是原型继承 现代项目开发,兼容性良好(ES6+)

总结

  1. 原型链是 JavaScript 继承的基础,所有继承方式均围绕原型链展开;
  2. 寄生组合式继承是 "完美方案",ES6 class extends 是其语法糖,推荐现代项目优先使用;
  3. 简单场景可使用原型式 / 寄生式继承,兼容旧环境可使用组合继承,仅需实例属性继承可使用构造函数继承。
相关推荐
Mapmost5 小时前
【高斯泼溅】从“看清”到“看懂”,3DGS语义化让数字孪生“会说话”
前端
指尖跳动的光5 小时前
防止前端页面重复请求
前端·javascript
luquinn5 小时前
用canvas切图展示及标记在原图片中的位置
开发语言·前端·javascript
巧克力芋泥包5 小时前
前端vue3调取阿里的oss存储
前端
AAA阿giao5 小时前
React Hooks 详解:从 useState 到 useEffect,彻底掌握函数组件的“灵魂”
前端·javascript·react.js
RedHeartWWW5 小时前
Next.js Middleware 极简教程
前端
饼饼饼5 小时前
从 0 到 1:前端 CI/CD 实战 ( 第一篇: 云服务器环境搭建)
运维·前端·自动化运维
用户47949283569156 小时前
给前端明星开源项目Biome提 PR,被 Snapshot 测试坑了一把
前端·后端·测试
૮・ﻌ・6 小时前
小兔鲜电商项目(一):项目准备、Layout模块、Home模块
前端·javascript·vue