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(); // 动物名称:小狗
优点与缺点
-
优点:实现简单,子类可继承父类原型上的所有方法;
-
缺点:
- 父类的引用类型实例属性会被所有子类实例共享(一个实例修改会影响其他实例);
- 无法向父类构造函数传递参数(子类实例创建时,无法自定义父类实例属性)。
二、构造函数继承(借用父类构造函数)
核心原理
在子类构造函数中,通过 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); // 中华田园犬
优点与缺点
-
优点:
- 解决了原型链继承中引用类型属性共享的问题;
- 可以向父类构造函数传递参数;
-
缺点:
- 只能继承父类的实例属性和方法,无法继承父类原型上的方法(每个子类实例都会复制一份父类方法,浪费内存);
- 子类实例无法共享父类方法,违背原型链的设计初衷。
三、组合继承(原型链 + 构造函数,最常用)
核心原理
结合原型链继承和构造函数继承的优点:
- 用原型链继承继承父类原型上的方法(实现方法共享);
- 用构造函数继承继承父类的实例属性(避免引用类型共享,支持传参)。
代码示例
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 检测正常)
优点与缺点
-
优点:
- 兼顾了原型链继承和构造函数继承的优点,既实现了方法共享,又避免了引用类型属性共享;
- 支持向父类传参,
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); // 中华田园犬
优点与缺点
-
优点:无需定义构造函数,实现简单,适合快速创建基于已有对象的新对象;
-
缺点:
- 引用类型属性会被所有新对象共享(与原型链继承一致);
- 无法向父对象传递参数,只能在创建新对象后手动修改属性。
五、寄生式继承(原型式继承的增强版)
核心原理
在原型式继承的基础上,封装一个创建对象的函数,在函数内部为新对象添加自有属性和方法,增强新对象的功能,最后返回新对象。
代码示例
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(); // 犬种/品种:中华田园犬
优点与缺点
-
优点:无需定义构造函数,可灵活增强新对象的功能,实现简单;
-
缺点:
- 引用类型属性共享问题依然存在;
- 每个新对象的自有方法都是独立的(无法共享),浪费内存;
- 无法实现方法的复用,类似构造函数继承的缺点。
六、寄生组合式继承(完美继承方案)
核心原理
结合组合继承和寄生式继承的优点,解决组合继承中父类构造函数被调用两次的问题:
- 用寄生式继承继承父类的原型(仅继承原型方法,不调用父类构造函数);
- 用构造函数继承继承父类的实例属性(支持传参,避免引用类型共享)。
代码示例
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(构造函数指向正确)
优点与缺点
-
优点:
- 父类构造函数仅被调用一次,避免了内存冗余;
- 实现了方法共享,避免了引用类型属性共享;
- 支持向父类传参,
instanceof检测和构造函数指向均正常; - 是 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+) |
总结
- 原型链是 JavaScript 继承的基础,所有继承方式均围绕原型链展开;
- 寄生组合式继承是 "完美方案",ES6
class extends是其语法糖,推荐现代项目优先使用; - 简单场景可使用原型式 / 寄生式继承,兼容旧环境可使用组合继承,仅需实例属性继承可使用构造函数继承。