JavaScript继承

什么是继承?

继承是面向对象编程中的重要概念,允许一个对象或类继承另一个对象或类的属性和方法。通过继承,可以实现代码复用和层次化设计,提高代码的可维护性和扩展性。

JavaScript是一种基于原型的语言,与传统的基于类的语言(如Java、C++)在继承机制上有很大不同。

原型链继承

原型链是JavaScript中实现继承的最基本方式。每个对象都有一个原型对象,通过__proto__属性指向其原型。当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。

示例:原型链继承

// 父构造函数
function Animal(name) {
  this.name = name;
  this.eat = function() {
    console.log(`${this.name} is eating.`);
  };
}

// 子构造函数
function Dog(name, breed) {
  this.name = name;
  this.breed = breed;
}

// 建立原型链继承关系
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

// 添加子类特有的方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking.`);
};

// 创建实例
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.eat(); // 继承自Animal
myDog.bark(); // Dog特有的方法

原型链继承的优缺点

优点

  • 实现简单,易于理解
  • 能够继承父类的属性和方法

缺点

  • 所有子类实例共享父类原型上的引用类型属性,可能导致意外修改
  • 无法向父类构造函数传递参数

构造函数继承

构造函数继承是通过在子类构造函数中调用父类构造函数来实现继承的方式。使用call()apply()方法可以在子类构造函数中调用父类构造函数,从而继承父类的实例属性。

示例:构造函数继承

// 父构造函数
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

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

// 子构造函数
function Dog(name, breed) {
  // 调用父构造函数,继承实例属性
  Animal.call(this, name);
  this.breed = breed;
}

// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'Labrador');

dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white']

// dog1.eat(); // 报错,无法继承父类原型上的方法

构造函数继承的优缺点

优点

  • 避免了引用类型属性的共享问题
  • 可以向父类构造函数传递参数

缺点

  • 无法继承父类原型上的方法
  • 每个子类实例都有父类方法的副本,造成内存浪费

组合继承

组合继承结合了原型链继承和构造函数继承的优点,既可以继承父类原型上的方法,又可以避免引用类型属性的共享问题。

示例:组合继承

// 父构造函数
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

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

// 子构造函数
function Dog(name, breed) {
  // 构造函数继承,继承实例属性
  Animal.call(this, name);
  this.breed = breed;
}

// 原型链继承,继承原型方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

// 添加子类特有的方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking.`);
};

// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'Labrador');

dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white']

dog1.eat(); // Buddy is eating.
dog1.bark(); // Buddy is barking.

组合继承的优缺点

优点

  • 可以继承父类原型上的方法
  • 避免了引用类型属性的共享问题
  • 可以向父类构造函数传递参数

缺点

  • 父类构造函数会被调用两次(一次在创建子类原型时,一次在子类构造函数中)

寄生组合式继承

寄生组合式继承是组合继承的优化版本,通过创建一个中间对象来继承父类原型,避免了父类构造函数的重复调用。

示例:寄生组合式继承

// 父构造函数
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

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

// 子构造函数
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// 优化:使用Object.create创建原型对象,避免父类构造函数重复调用
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name} is barking.`);
};

// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // Buddy is eating.
dog.bark(); // Buddy is barking.

寄生组合式继承的优缺点

优点

  • 可以继承父类原型上的方法
  • 避免了引用类型属性的共享问题
  • 可以向父类构造函数传递参数
  • 父类构造函数只被调用一次,性能更好

缺点

  • 实现相对复杂

ES6类继承

ES6引入了class关键字,提供了更简洁、更接近传统面向对象语言的类语法。通过extends关键字可以实现类的继承。

示例:ES6类继承

// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ['black', 'white'];
  }

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

// 子类,继承自Animal
class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类构造函数
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} is barking.`);
  }
}

// 创建实例
const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // Buddy is eating.
dog.bark(); // Buddy is barking.

ES6类继承的特点

  • 使用extends关键字实现继承
  • 使用super关键字调用父类构造函数和方法
  • 子类可以重写父类的方法
  • 支持静态方法和属性的继承

多重继承

JavaScript不直接支持多重继承(一个类继承多个父类),但可以通过混合(Mixin)的方式实现类似效果。

示例:使用Mixin实现多重继承

// 定义Mixin
const CanFly = {
  fly() {
    console.log(`${this.name} is flying.`);
  }
};

const CanSwim = {
  swim() {
    console.log(`${this.name} is swimming.`);
  }
};

// 父类
class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 子类,通过Object.assign实现多重继承
class Duck extends Animal {
  constructor(name) {
    super(name);
    // 将Mixin的方法复制到实例上
    Object.assign(this, CanFly, CanSwim);
  }
}

// 创建实例
const duck = new Duck('Donald');
duck.fly(); // Donald is flying.
duck.swim(); // Donald is swimming.

继承的最佳实践

  1. 优先使用ES6类语法:ES6类语法更简洁、更易读,推荐优先使用。
  2. 合理设计继承层次:避免过深的继承层次,一般不超过3层。
  3. 使用组合优于继承:在许多情况下,组合(对象之间的关联关系)比继承更灵活。
  4. 遵循单一职责原则:每个类只负责一个功能领域。
  5. 避免过度使用继承:只有当子类确实是父类的一种类型时,才考虑使用继承。
  6. 使用super关键字:在子类构造函数中,一定要调用super(),且必须在访问this之前调用。
  7. 正确设置原型链:使用Object.create()extends确保原型链的正确性。

总结

JavaScript提供了多种继承实现方式,从早期的原型链继承到ES6的类继承,每种方式都有其优缺点:

  • 原型链继承:简单但存在引用类型共享问题
  • 构造函数继承:解决了引用类型共享问题,但无法继承原型方法
  • 组合继承:结合了前两种方式的优点,但父类构造函数会被调用两次
  • 寄生组合式继承:优化了组合继承,避免了父类构造函数的重复调用
  • ES6类继承:提供了更简洁的语法,是当前推荐的继承方式

在实际开发中,应根据具体需求选择合适的继承方式,优先考虑使用ES6类语法。

练习

  1. 使用ES6类语法创建一个Person类,包含nameage属性,以及sayHello方法。
  2. 创建一个Student类,继承自Person类,添加grade属性和study方法。
  3. 创建一个Teacher类,继承自Person类,添加subject属性和teach方法。
  4. 创建StudentTeacher的实例,验证继承关系是否正确。
  5. 尝试使用Mixin为Student类添加额外的功能(如playSport方法)。
« 上一篇 JavaScript类 下一篇 » JavaScript模块