ES5 实现继承的 5 种方式(原理 + 代码 + 优缺点)
ES5 没有 class 和 extends 关键字,需基于 原型链(prototype) 和 构造函数 手动实现继承。核心思路是:让子类的原型指向父类的实例(或原型) ,同时确保子类能访问父类的属性和方法,且不破坏自身的构造逻辑。
以下是 ES5 中 5 种主流继承方式,从基础到进阶,逐一解析原理、实现和适用场景:
一、核心概念铺垫
在理解继承前,需明确 3 个关键概念:
- 构造函数:用于创建对象的函数(如
function Parent() {}),通过new生成实例; - 原型(
prototype) :构造函数的属性,存储所有实例共享的方法(如Parent.prototype.say = function() {}); - 实例的
__proto__:指向其构造函数的prototype(如new Parent().__proto__ === Parent.prototype),是原型链查找的核心。
继承的本质是:让子类实例的原型链能找到父类的原型,从而复用父类的属性和方法。
二、1. 原型链继承(最基础)
原理
让 子类的原型(Child.prototype)指向父类的实例(new Parent()) ,子类实例通过原型链访问父类的属性和方法。
实现代码
// 父类:动物(构造函数+原型方法)
function Parent(name) {
this.name = name; // 父类实例属性
this.colors = ['red', 'blue']; // 父类引用类型属性
}
// 父类原型方法(所有实例共享)
Parent.prototype.sayName = function() {
console.log('父类名字:', this.name);
};
// 子类:猫(继承 Parent)
function Child(age) {
this.age = age; // 子类实例属性
}
// 核心:子类原型指向父类实例(建立原型链)
Child.prototype = new Parent();
// 修复子类原型的 constructor 指向(否则 Child.prototype.constructor 会指向 Parent)
Child.prototype.constructor = Child;
// 子类原型方法(可扩展自己的方法)
Child.prototype.sayAge = function() {
console.log('子类年龄:', this.age);
};
// 测试
const child1 = new Child(2);
const child2 = new Child(3);
// 访问父类实例属性
console.log(child1.name); // undefined(父类构造函数的 name 未传参)
child1.name = '小花';
console.log(child1.name); // '小花'
// 访问父类原型方法
child1.sayName(); // 父类名字:小花
// 访问子类属性和方法
child1.sayAge(); // 子类年龄:2
// 问题暴露:父类引用类型属性被所有子类实例共享
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue', 'green'](child2 被影响了!)
优点
- 实现简单,直接通过原型链复用父类方法;
- 子类实例既是子类的实例,也是父类的实例(
child1 instanceof Parent === true)。
缺点(致命问题)
- 父类引用类型属性被所有子类实例共享:如
colors数组,一个实例修改会影响所有实例; - 子类实例化时无法向父类构造函数传参:如
new Child(2)无法直接给Parent的name传值,需手动补充; - 子类原型的
constructor需手动修复:否则会指向父类,破坏原型链完整性。
适用场景
- 父类无引用类型属性;
- 子类实例无需向父类传参;
- 简单场景(如仅复用父类方法)。
二、2. 构造函数继承(解决传参和引用共享问题)
原理
在 子类构造函数中通过 call/apply 调用父类构造函数,让父类的属性和方法绑定到子类实例上(而非原型上),从而避免引用类型共享,同时支持传参。
实现代码
// 父类:动物
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue']; // 引用类型属性
this.sayName = function() { // 父类实例方法(非原型方法)
console.log('父类名字:', this.name);
};
}
// 子类:猫
function Child(name, age) {
// 核心:调用父类构造函数,绑定子类实例(this 指向 child)
Parent.call(this, name); // 向父类传参 name
this.age = age; // 子类自身属性
}
// 子类原型方法
Child.prototype.sayAge = function() {
console.log('子类年龄:', this.age);
};
// 测试
const child1 = new Child('小花', 2);
const child2 = new Child('小黑', 3);
// 访问父类属性(不共享)
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'](无影响,解决引用共享问题)
// 访问父类方法
child1.sayName(); // 父类名字:小花
child2.sayName(); // 父类名字:小黑
// 子类实例化时可向父类传参
console.log(child1.name); // '小花'
// 问题暴露:无法复用父类的原型方法
Parent.prototype.sayHi = function() {
console.log('父类原型方法:Hi');
};
child1.sayHi(); // 报错:sayHi is not a function(子类原型链找不到父类原型)
优点
- 解决引用类型属性共享问题:每个子类实例都有父类属性的独立副本;
- 支持子类向父类传参:通过
call(this, 参数)灵活传参; - 无需修复
constructor:子类原型未被修改,child1.constructor === Child。
缺点
- 无法复用父类的原型方法:父类方法需定义在构造函数内(而非原型上),导致每个实例都有方法副本,浪费内存;
- 子类实例不是父类的实例:
child1 instanceof Parent === false(原型链未关联)。
适用场景
- 父类有引用类型属性,需避免实例共享;
- 子类实例需向父类传参;
- 无需复用父类原型方法的场景。
三、3. 组合继承(原型链 + 构造函数,最常用)
原理
结合 原型链继承 和 构造函数继承 的优点:
- 用「原型链继承」复用父类的原型方法(节省内存);
- 用「构造函数继承」初始化父类实例属性(避免共享 + 支持传参)。
实现代码
// 父类:动物
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue']; // 引用类型属性
}
// 父类原型方法(共享)
Parent.prototype.sayName = function() {
console.log('父类名字:', this.name);
};
// 子类:猫
function Child(name, age) {
// 1. 构造函数继承:初始化父类属性(避免共享+传参)
Parent.call(this, name);
this.age = age;
}
// 2. 原型链继承:复用父类原型方法
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;
// 子类原型方法
Child.prototype.sayAge = function() {
console.log('子类年龄:', this.age);
};
// 测试
const child1 = new Child('小花', 2);
const child2 = new Child('小黑', 3);
// 1. 引用类型属性不共享
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'](正确)
// 2. 支持向父类传参
console.log(child1.name); // '小花'(正确)
// 3. 复用父类原型方法
child1.sayName(); // 父类名字:小花(正确)
// 4. 子类实例既是子类也是父类的实例
console.log(child1 instanceof Child); // true
console.log(child1 instanceof Parent); // true
// 问题暴露:父类构造函数被调用两次
// - 第一次:new Parent() 时(Child.prototype = new Parent())
// - 第二次:Parent.call(this, name) 时(子类实例化时)
// 导致父类构造函数内的属性被初始化两次(但不影响使用,仅轻微浪费性能)
优点
- 兼顾前两种方式的优点:复用原型方法、避免引用共享、支持传参;
- 原型链完整:子类实例既是子类也是父类的实例;
- 是 ES5 中最实用的继承方式。
缺点
- 父类构造函数被调用两次:一次是子类原型初始化时,一次是子类实例化时(导致父类构造函数内的属性被重复初始化,但不影响功能)。
适用场景
- 大多数 ES5 继承场景(如管理系统、工具类复用);
- 需复用父类原型方法、支持传参、避免引用共享的核心场景。
四、4. 寄生组合继承(完美继承,解决组合继承的缺点)
原理
对组合继承的优化:用「空构造函数」作为中间媒介,避免父类构造函数被调用两次。核心逻辑:
- 用
Object.create(Parent.prototype)创建一个「空对象」(该对象的__proto__指向父类原型); - 让子类原型指向这个空对象,而非直接指向
new Parent()(避免调用父类构造函数); - 用构造函数继承初始化父类属性(支持传参 + 避免共享)。
实现代码
// 父类:动物
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log('父类名字:', this.name);
};
// 子类:猫
function Child(name, age) {
// 构造函数继承:初始化父类属性(仅调用一次父类构造函数)
Parent.call(this, name);
this.age = age;
}
// 核心优化:用 Object.create 创建中间对象(避免调用父类构造函数)
// Object.create(obj) → 返回一个新对象,其 __proto__ 指向 obj
Child.prototype = Object.create(Parent.prototype);
// 修复 constructor 指向
Child.prototype.constructor = Child;
// 子类原型方法
Child.prototype.sayAge = function() {
console.log('子类年龄:', this.age);
};
// 测试(效果与组合继承一致,但父类构造函数仅调用一次)
const child1 = new Child('小花', 2);
console.log(child1.colors); // ['red', 'blue']
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue'](无共享)
child1.sayName(); // 父类名字:小花(复用原型方法)
console.log(child1 instanceof Parent); // true(原型链完整)
// 验证父类构造函数调用次数:仅子类实例化时调用一次(Parent.call)
优点
- 完美解决组合继承的缺点:父类构造函数仅调用一次;
- 保留组合继承的所有优势:复用原型方法、避免引用共享、支持传参、原型链完整;
- 是 ES5 中「最理想的继承方式」,也是许多库(如 jQuery)的底层实现。
缺点
- 代码比组合继承稍繁琐(需手动创建中间对象 + 修复
constructor); - 低版本 IE(IE8 及以下)不支持
Object.create(需兼容处理,见下文)。
适用场景
- 生产环境的核心继承场景(如框架、工具类);
- 对性能有要求,需避免父类构造函数重复调用的场景。
兼容 IE8 的 Object.create 实现
IE8 及以下不支持原生 Object.create,可手动 polyfill:
if (!Object.create) {
Object.create = function(proto) {
// 创建空构造函数
function F() {}
// 让空构造函数的原型指向父类原型
F.prototype = proto;
// 返回空构造函数的实例(其 __proto__ 指向 proto)
return new F();
};
}
五、5. 寄生式继承(增强对象的继承)
原理
基于一个现有对象(或构造函数实例),通过「添加新方法 / 属性」增强对象,返回增强后的新对象。本质是「对象增强」,而非严格意义上的 “类继承”。
实现代码
// 父类:动物
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log('父类名字:', this.name);
};
// 寄生式继承函数:增强对象
function createChild(original, age) {
// 1. 克隆父类实例(用 Object.create 实现原型链关联)
const clone = Object.create(original.prototype);
// 2. 增强对象:添加子类属性和方法
clone.age = age;
clone.sayAge = function() {
console.log('子类年龄:', this.age);
};
// 3. 返回增强后的对象
return clone;
}
// 测试
const parent = new Parent('动物');
const child = createChild(parent, 2);
// 访问父类属性和方法
console.log(child.name); // undefined(需手动赋值,或在增强时传参)
child.name = '小花';
child.sayName(); // 父类名字:小花
// 访问增强后的属性和方法
child.sayAge(); // 子类年龄:2
// 原型链关联
console.log(child instanceof Parent); // true
优点
- 实现简单,无需定义子类构造函数;
- 灵活增强对象,可按需添加属性和方法。
缺点
- 无法复用增强后的方法:每个实例都有独立的方法副本(如
sayAge),浪费内存; - 本质是 “对象克隆 + 增强”,而非严格的类继承(无子类构造函数)。
适用场景
- 临时增强某个对象(如对第三方库的对象扩展功能);
- 无需复用方法,仅需简单扩展属性的场景。
六、5 种继承方式对比表
| 继承方式 | 核心逻辑 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 原型链继承 | 子类原型 = 父类实例 | 简单,复用父类原型方法 | 引用共享、无法传参、需修 constructor | 简单场景,无引用属性 |
| 构造函数继承 | 子类构造函数 call 父类 | 避免共享、支持传参 | 无法复用父类原型方法、非父类实例 | 有引用属性,需传参 |
| 组合继承 | 原型链 + 构造函数 | 复用方法、支持传参、避免共享 | 父类构造函数调用两次 | 大多数 ES5 场景(推荐入门) |
| 寄生组合继承 | Object.create + 构造函数 | 完美继承(无上述缺点) | 代码稍繁琐、IE8 需兼容 | 生产环境、框架 / 工具类(推荐) |
| 寄生式继承 | 克隆对象 + 增强 | 灵活、无需定义子类 | 方法无法复用、非严格类继承 | 临时扩展对象功能 |
七、ES5 继承核心总结
-
核心本质:通过原型链让子类实例访问父类的属性和方法,通过构造函数确保实例属性的独立性;
-
推荐优先级:寄生组合继承 > 组合继承 > 构造函数继承 > 原型链继承 > 寄生式继承;
-
关键注意点:
- 原型链继承需修复
constructor指向; - 构造函数继承无法复用原型方法;
- 组合继承需接受父类构造函数调用两次的轻微性能损耗;
- 寄生组合继承是 ES5 最理想的方式,需兼容 IE8 时添加
Object.createpolyfill。
- 原型链继承需修复
八、ES5 继承 vs ES6 class 继承
ES6 的 class extends 本质是 寄生组合继承的语法糖,底层逻辑与 ES5 寄生组合继承一致,但更简洁:
// ES6 class 继承(等价于 ES5 寄生组合继承)
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() { console.log(this.name); }
}
class Child extends Parent {
constructor(name, age) {
super(name); // 等价于 Parent.call(this, name)
this.age = age;
}
sayAge() { console.log(this.age); }
}
ES6 简化了 prototype 和 constructor 的手动处理,让继承更直观,但底层仍是 ES5 的原型链逻辑。