Skip to content

原型继承:JavaScript 中的多种继承模式详解

与其他基于类的编程语言不同,JavaScript 的继承机制是建立在对象之间的链接之上的。这种机制被称为原型继承。当我们访问一个对象的属性时,如果对象本身没有,引擎会自动沿着这个链接向上查找。理解这种委托机制,是掌握 JavaScript 独特面向对象特性的关键。

原型继承的基本概念

原型继承是 JavaScript 实现对象间继承关系的一种方式。通过原型继承,一个对象可以访问另一个对象(其原型)的属性和方法。这种继承方式不同于传统面向对象语言中的类继承,而是基于对象之间的委托关系。

基本原型继承

javascript
// 父对象
const animal = {
  type: "animal",
  speak() {
    console.log(`${this.type} makes a sound`);
  },
  eat() {
    console.log(`${this.type} is eating`);
  },
};

// 通过Object.create()创建继承animal的对象
const dog = Object.create(animal);
dog.type = "dog";
dog.bark = function () {
  console.log("Woof! Woof!");
};

// dog继承了animal的方法
dog.speak(); // dog makes a sound
dog.eat(); // dog is eating
dog.bark(); // Woof! Woof!

// 检查原型关系
console.log(dog.__proto__ === animal); // true

原型链继承模式

1. 简单原型继承

这是最基本的继承方式,直接将子构造函数的原型指向父构造函数的实例。

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

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

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

// 关键:将Child的原型指向Parent的实例
Child.prototype = new Parent();

const child1 = new Child("John", 25);
const child2 = new Child("Sarah", 30);

child1.sayName(); // undefined (因为Parent构造函数执行时没有传入name)
child1.colors.push("yellow");

// 问题:引用类型的属性会被所有实例共享
console.log(child2.colors); // ['red', 'green', 'blue', 'yellow']

问题分析:

  • 引用类型的属性会被所有实例共享
  • 创建子实例时无法向父构造函数传递参数

2. 构造函数继承(借用构造函数)

为了解决原型继承的问题,出现了构造函数继承模式。

javascript
function Parent(name, colors) {
  this.name = name;
  this.colors = colors;
  this.sayName = function () {
    console.log(this.name);
  };
}

function Child(name, age, colors) {
  // 关键:借用Parent构造函数,使用call或apply
  Parent.call(this, name, colors);
  this.age = age;
}

const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);

child1.sayName(); // John
child2.sayName(); // Sarah

// 各自拥有独立的colors数组
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']

// 问题:方法都在构造函数中定义,无法实现方法复用
console.log(child1.sayName === child2.sayName); // false

优点:

  • 解决了引用类型共享的问题
  • 可以向父构造函数传递参数

缺点:

  • 方法在构造函数中定义,每次创建实例都会重新创建
  • 无法实现函数复用

3. 组合继承(伪经典继承)

组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承模式。

javascript
function Parent(name, colors) {
  this.name = name;
  this.colors = colors;
}

// 方法在原型上定义
Parent.prototype.sayName = function () {
  console.log(this.name);
};

Parent.prototype.sayColors = function () {
  console.log(this.colors.join(", "));
};

function Child(name, age, colors) {
  // 继承属性(借用构造函数)
  Parent.call(this, name, colors);
  this.age = age;
}

// 继承方法(原型链)
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修复constructor指向

// 添加子类特有的方法
Child.prototype.sayAge = function () {
  console.log(`I am ${this.age} years old`);
};

const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);

// 验证继承
child1.sayName(); // John
child1.sayColors(); // red, green
child1.sayAge(); // I am 25 years old

// 引用类型独立
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']

// 方法复用
console.log(child1.sayName === child2.sayName); // true
console.log(child1.sayColors === child2.sayColors); // true

组合继承的改进版本:

javascript
function Parent(name, colors) {
  this.name = name;
  this.colors = colors;
}

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

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

// 不使用new Parent(),而是直接使用Object.create
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.sayAge = function () {
  console.log(`I am ${this.age} years old`);
};

这种改进版本避免了调用父构造函数两次的问题。

4. 寄生式继承

寄生式继承创建一个封装继承过程的函数,在内部以某种方式增强对象。

javascript
function createAnother(original) {
  // 创建一个新对象,继承original
  const clone = Object.create(original);

  // 增强对象
  clone.sayHi = function () {
    console.log("Hi!");
  };

  return clone;
}

const person = {
  name: "John",
  friends: ["Tom", "Jerry"],
};

const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi!

console.log(anotherPerson.name); // John
console.log(anotherPerson.friends); // ['Tom', 'Jerry']

// 注意:引用类型仍然是共享的
anotherPerson.friends.push("Bob");
console.log(person.friends); // ['Tom', 'Jerry', 'Bob']

5. 寄生组合式继承

这是最理想的继承方式,解决了组合继承调用两次父构造函数的问题。

javascript
function inheritPrototype(child, parent) {
  // 创建父类原型的副本
  const prototype = Object.create(parent.prototype);
  // 修复constructor指向
  prototype.constructor = child;
  // 设置子类原型
  child.prototype = prototype;
}

function Parent(name, colors) {
  this.name = name;
  this.colors = colors;
}

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

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

// 使用寄生组合式继承
inheritPrototype(Child, Parent);

// 添加子类方法
Child.prototype.sayAge = function () {
  console.log(`I am ${this.age} years old`);
};

const child = new Child("John", 25, ["red", "green"]);
child.sayName(); // John
child.sayAge(); // I am 25 years old

寄生组合式继承的优势:

  • 只调用一次父构造函数
  • 避免在子类原型上创建不必要的属性
  • 保持原型链不变
  • 是最理想的继承范式

实际应用中的继承模式

1. 实现接口(Interface)

JavaScript 没有原生的接口概念,但可以通过原型继承模拟:

javascript
// 定义接口原型
const Drawable = {
  draw() {
    throw new Error("draw method must be implemented");
  },
};

const Movable = {
  move(x, y) {
    throw new Error("move method must be implemented");
  },
};

// 创建实现接口的对象
function Shape(x, y) {
  this.x = x;
  this.y = y;
}

// 混入接口
Object.assign(Shape.prototype, Drawable, Movable);

// 具体类
function Circle(x, y, radius) {
  Shape.call(this, x, y);
  this.radius = radius;
}

// 继承Shape并实现接口方法
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.draw = function () {
  console.log(
    `Drawing circle at (${this.x}, ${this.y}) with radius ${this.radius}`
  );
};

Circle.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.log(`Circle moved to (${this.x}, ${this.y})`);
};

const circle = new Circle(10, 10, 5);
circle.draw(); // Drawing circle at (10, 10) with radius 5
circle.move(5, 5); // Circle moved to (15, 15)

2. 实现 Mixin 模式

Mixin 允许将多个对象的功能组合到一个对象中:

javascript
// 定义多个Mixin
const canEat = {
  eat() {
    console.log(`${this.name} is eating`);
  },
};

const canWalk = {
  walk() {
    console.log(`${this.name} is walking`);
  },
};

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

// 创建基础对象
function Animal(name) {
  this.name = name;
}

// 应用Mixin
function applyMixins(constructor, mixins) {
  mixins.forEach((mixin) => {
    Object.assign(constructor.prototype, mixin);
  });
}

// 不同的动物组合不同的能力
function Dog(name) {
  Animal.call(this, name);
}

function Fish(name) {
  Animal.call(this, name);
}

function Duck(name) {
  Animal.call(this, name);
}

applyMixins(Dog, [canEat, canWalk]);
applyMixins(Fish, [canEat, canSwim]);
applyMixins(Duck, [canEat, canWalk, canSwim]);

const dog = new Dog("Buddy");
dog.eat(); // Buddy is eating
dog.walk(); // Buddy is walking

const fish = new Fish("Nemo");
fish.eat(); // Nemo is eating
fish.swim(); // Nemo is swimming

const duck = new Duck("Donald");
duck.eat(); // Donald is eating
duck.walk(); // Donald is walking
duck.swim(); // Donald is swimming

3. 实现装饰器模式

使用原型继承来增强现有对象的功能:

javascript
// 基础组件
function Coffee() {
  this.cost = 2;
  this.description = "Black coffee";
}

Coffee.prototype.getDescription = function () {
  return this.description;
};

Coffee.prototype.getCost = function () {
  return this.cost;
};

// 装饰器基础
function CoffeeDecorator(coffee) {
  this.coffee = coffee;
}

CoffeeDecorator.prototype.getDescription = function () {
  return this.coffee.getDescription();
};

CoffeeDecorator.prototype.getCost = function () {
  return this.coffee.getCost();
};

// 具体装饰器
function MilkDecorator(coffee) {
  CoffeeDecorator.call(this, coffee);
  this.cost = 0.5;
  this.description = "milk";
}

MilkDecorator.prototype = Object.create(CoffeeDecorator.prototype);
MilkDecorator.prototype.constructor = MilkDecorator;

MilkDecorator.prototype.getDescription = function () {
  return `${this.coffee.getDescription()}, ${this.description}`;
};

MilkDecorator.prototype.getCost = function () {
  return this.coffee.getCost() + this.cost;
};

// 使用装饰器
const coffee = new Coffee();
const coffeeWithMilk = new MilkDecorator(coffee);

console.log(coffee.getDescription()); // Black coffee
console.log(coffee.getCost()); // 2

console.log(coffeeWithMilk.getDescription()); // Black coffee, milk
console.log(coffeeWithMilk.getCost()); // 2.5

性能考虑和最佳实践

1. 避免过深的原型链

javascript
// ❌ 避免过深的原型链
function Level1() {}
function Level2() {}
function Level3() {}
function Level4() {}
function Level5() {}

Level2.prototype = Object.create(Level1.prototype);
Level3.prototype = Object.create(Level2.prototype);
Level4.prototype = Object.create(Level3.prototype);
Level5.prototype = Object.create(Level4.prototype);

// ✅ 使用组合而非深继承
class Level5 {
  constructor(level1, level2, level3, level4) {
    this.level1 = level1;
    this.level2 = level2;
    this.level3 = level3;
    this.level4 = level4;
  }
}

2. 合理放置属性和方法

javascript
function Car(brand, model) {
  // 实例独有的属性放在构造函数中
  this.brand = brand;
  this.model = model;
  this.odometer = 0;
}

// 共享的方法放在原型上
Car.prototype.drive = function (distance) {
  this.odometer += distance;
  console.log(`${this.brand} ${this.model} drove ${distance} km`);
};

// 私有方法可以通过闭包实现
const CarFactory = (function () {
  function validateBrand(brand) {
    return typeof brand === "string" && brand.length > 0;
  }

  return function (brand, model) {
    if (!validateBrand(brand)) {
      throw new Error("Invalid brand");
    }
    this.brand = brand;
    this.model = model;
  };
})();

3. 使用现代 Class 语法

虽然理解原型继承很重要,但在现代项目中,推荐使用 Class 语法:

javascript
// 现代的写法(底层仍然是原型继承)
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks: Woof!`);
  }

  fetch() {
    console.log(`${this.name} is fetching the ball`);
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks: Woof!
dog.fetch(); // Buddy is fetching the ball

总结

JavaScript 中的原型继承提供了灵活的对象间继承机制。了解不同的继承模式有助于我们在合适的场景选择合适的方案:

  • 原型链继承:简单但有问题,适合简单场景
  • 构造函数继承:解决了引用类型共享问题,但无法复用方法
  • 组合继承:最常用,结合了前两者的优点
  • 寄生组合式继承:最理想的继承范式
  • Mixin 模式:适合多重继承场景
  • 装饰器模式:适合动态扩展功能

虽然现代 JavaScript 提供了 Class 语法,但理解原型继承的底层机制对于深入掌握 JavaScript 仍然至关重要。在实际开发中,要根据具体需求选择最合适的继承模式。