JavaScript 继承方式对比

原型链、构造函数、组合继承、寄生组合、ES6 Class 的实现与优缺点

问题

JavaScript 有哪几种继承方式?各自的优缺点是什么?

解答

1. 原型链继承

function Parent() {
  this.colors = ['red', 'blue'];
}

Parent.prototype.getColors = function() {
  return this.colors;
};

function Child() {}

// 子类原型指向父类实例
Child.prototype = new Parent();

// 测试
const child1 = new Child();
const child2 = new Child();

child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue', 'green'] - 引用类型被共享

优点:简单,能继承父类原型上的方法

缺点

  • 引用类型属性被所有实例共享
  • 无法向父类构造函数传参

2. 构造函数继承

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 调用父类构造函数
  Parent.call(this, name);
  this.age = age;
}

// 测试
const child1 = new Child('Tom', 18);
const child2 = new Child('Jerry', 20);

child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] - 不会被共享

console.log(child1.getName); // undefined - 无法继承原型方法

优点

  • 避免引用类型属性共享
  • 可以向父类传参

缺点

  • 无法继承父类原型上的方法
  • 方法在构造函数中定义,每次创建实例都会创建一遍

3. 组合继承

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 第二次调用 Parent
  this.age = age;
}

Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;

// 测试
const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child.colors);    // ['red', 'blue']

优点:融合原型链和构造函数的优点

缺点:父类构造函数被调用两次,子类原型上有多余的父类属性

4. 寄生组合继承(推荐)

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 核心:创建父类原型的副本,避免调用父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 测试
const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child instanceof Parent); // true

优点

  • 只调用一次父类构造函数
  • 原型链保持完整
  • 是最理想的继承方式

5. ES6 Class 继承

class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
  }

  getName() {
    return this.name;
  }

  static sayHello() {
    console.log('Hello');
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用 super
    this.age = age;
  }

  getAge() {
    return this.age;
  }
}

// 测试
const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child.getAge());  // 18

Child.sayHello(); // 'Hello' - 静态方法也能继承

优点

  • 语法清晰,易于理解
  • 内置支持静态方法继承
  • 本质是寄生组合继承的语法糖

缺点

  • 需要 ES6 环境或转译

对比总结

继承方式优点缺点
原型链简单引用类型共享,无法传参
构造函数可传参,不共享无法继承原型方法
组合继承功能完整调用两次父类构造函数
寄生组合效率高,功能完整实现稍复杂
ES6 Class语法简洁需要 ES6 环境

关键点

  • 原型链继承Child.prototype = new Parent(),问题是引用类型共享
  • 构造函数继承Parent.call(this),问题是无法继承原型方法
  • 组合继承:结合两者,但父类构造函数执行两次
  • 寄生组合继承:用 Object.create(Parent.prototype) 替代 new Parent(),是最优方案
  • ES6 Classextends + super(),本质是寄生组合继承的语法糖