实现类的继承

理解 JavaScript 中实现类继承的多种方式,包括原型链继承、构造函数继承、组合继承等经典模式

问题

在 JavaScript 中实现类的继承机制,使子类能够继承父类的属性和方法。需要掌握多种继承方式的实现原理、优缺点,以及 ES6 class 语法糖背后的实现机制。

解答

方式一:原型链继承

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

Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(age) {
  this.age = age;
}

// 实现继承
Child.prototype = new Parent('parent');
Child.prototype.constructor = Child;

方式二:构造函数继承(借用构造函数)

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;
}

方式三:组合继承(推荐)

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;
}

// 继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

方式四:寄生组合式继承(最优解)

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 避免调用两次父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

方式五:ES6 Class 继承

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); // 调用父类构造函数
    this.age = age;
  }
  
  sayAge() {
    console.log(this.age);
  }
}

通用继承函数封装

/**
 * 实现寄生组合式继承
 * @param {Function} Child - 子类构造函数
 * @param {Function} Parent - 父类构造函数
 */
function inherit(Child, Parent) {
  // 创建父类原型的副本
  Child.prototype = Object.create(Parent.prototype);
  // 修正构造函数指向
  Child.prototype.constructor = Child;
  // 保存父类引用(可选)
  Child.super = Parent;
}

// 使用示例
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

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

inherit(Child, Parent);

使用示例

// 寄生组合式继承示例
function Animal(name) {
  this.name = name;
  this.energy = 100;
}

Animal.prototype.eat = function() {
  this.energy += 10;
  console.log(`${this.name} is eating, energy: ${this.energy}`);
};

Animal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping`);
};

function Dog(name, breed) {
  // 继承属性
  Animal.call(this, name);
  this.breed = breed;
}

// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类特有方法
Dog.prototype.bark = function() {
  this.energy -= 5;
  console.log(`${this.name} is barking! Woof! Energy: ${this.energy}`);
};

// 测试
const dog = new Dog('旺财', '哈士奇');
console.log(dog.name); // 旺财
console.log(dog.breed); // 哈士奇
dog.eat(); // 旺财 is eating, energy: 110
dog.bark(); // 旺财 is barking! Woof! Energy: 105
dog.sleep(); // 旺财 is sleeping

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
// ES6 Class 继承示例
class Shape {
  constructor(color) {
    this.color = color;
  }
  
  getColor() {
    return this.color;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color); // 必须先调用 super
    this.radius = radius;
  }
  
  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

const circle = new Circle('red', 5);
console.log(circle.getColor()); // red
console.log(circle.getArea()); // 78.53981633974483

关键点

  • 原型链继承:子类原型指向父类实例,缺点是所有实例共享引用类型属性,且无法向父类构造函数传参

  • 构造函数继承:通过 call/apply 调用父类构造函数,解决了属性共享问题,但无法继承父类原型上的方法

  • 组合继承:结合原型链和构造函数继承,是最常用的方式,缺点是会调用两次父类构造函数

  • 寄生组合式继承:使用 Object.create() 创建父类原型副本,避免调用两次构造函数,是最优的继承方案

  • 关键步骤

    1. 使用 Parent.call(this) 继承实例属性
    2. 使用 Object.create(Parent.prototype) 继承原型方法
    3. 修正 constructor 指向子类
  • ES6 Class:本质是语法糖,底层仍是原型继承,但语法更清晰,必须在子类构造函数中先调用 super()

  • instanceof 检测:正确实现继承后,子类实例既是子类的实例,也是父类的实例

  • 注意事项:修正 constructor 指向很重要,否则会影响实例的构造函数判断