ES5怎么实现继承(5 种方式)

52 阅读10分钟

ES5 实现继承的 5 种方式(原理 + 代码 + 优缺点)

ES5 没有 class 和 extends 关键字,需基于 原型链(prototype  和 构造函数 手动实现继承。核心思路是:让子类的原型指向父类的实例(或原型) ,同时确保子类能访问父类的属性和方法,且不破坏自身的构造逻辑。

以下是 ES5 中 5 种主流继承方式,从基础到进阶,逐一解析原理、实现和适用场景:

一、核心概念铺垫

在理解继承前,需明确 3 个关键概念:

  1. 构造函数:用于创建对象的函数(如 function Parent() {}),通过 new 生成实例;
  2. 原型(prototype :构造函数的属性,存储所有实例共享的方法(如 Parent.prototype.say = function() {});
  3. 实例的 __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)。

缺点(致命问题)

  1. 父类引用类型属性被所有子类实例共享:如 colors 数组,一个实例修改会影响所有实例;
  2. 子类实例化时无法向父类构造函数传参:如 new Child(2) 无法直接给 Parent 的 name 传值,需手动补充;
  3. 子类原型的 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(子类原型链找不到父类原型)

优点

  1. 解决引用类型属性共享问题:每个子类实例都有父类属性的独立副本;
  2. 支持子类向父类传参:通过 call(this, 参数) 灵活传参;
  3. 无需修复 constructor:子类原型未被修改,child1.constructor === Child

缺点

  1. 无法复用父类的原型方法:父类方法需定义在构造函数内(而非原型上),导致每个实例都有方法副本,浪费内存;
  2. 子类实例不是父类的实例child1 instanceof Parent === false(原型链未关联)。

适用场景

  • 父类有引用类型属性,需避免实例共享;
  • 子类实例需向父类传参;
  • 无需复用父类原型方法的场景。

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

原理

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

  1. 用「原型链继承」复用父类的原型方法(节省内存);
  2. 用「构造函数继承」初始化父类实例属性(避免共享 + 支持传参)。

实现代码

// 父类:动物
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. 寄生组合继承(完美继承,解决组合继承的缺点)

原理

对组合继承的优化:用「空构造函数」作为中间媒介,避免父类构造函数被调用两次。核心逻辑:

  1. 用 Object.create(Parent.prototype) 创建一个「空对象」(该对象的 __proto__ 指向父类原型);
  2. 让子类原型指向这个空对象,而非直接指向 new Parent()(避免调用父类构造函数);
  3. 用构造函数继承初始化父类属性(支持传参 + 避免共享)。

实现代码

// 父类:动物
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 继承核心总结

  1. 核心本质:通过原型链让子类实例访问父类的属性和方法,通过构造函数确保实例属性的独立性;

  2. 推荐优先级:寄生组合继承 > 组合继承 > 构造函数继承 > 原型链继承 > 寄生式继承;

  3. 关键注意点

    • 原型链继承需修复 constructor 指向;
    • 构造函数继承无法复用原型方法;
    • 组合继承需接受父类构造函数调用两次的轻微性能损耗;
    • 寄生组合继承是 ES5 最理想的方式,需兼容 IE8 时添加 Object.create polyfill。

八、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 的原型链逻辑。