JavaScript 原型继承详解:从基础到实践

45 阅读4分钟

JavaScript 原型继承详解:从基础到实践

JavaScript 是一门基于原型(prototype)的编程语言,其继承机制与传统面向对象语言(如 Java 或 C++)中的类继承有本质区别。理解原型继承是掌握 JavaScript 面向对象编程的关键。本文将系统、详细地讲解 JavaScript 中的原型继承原理,涵盖构造函数、原型对象、call/apply 方法、原型链构建以及经典继承模式的实现。


一、JavaScript 对象与构造函数

在 JavaScript 中,对象可以通过多种方式创建,其中使用构造函数配合 new 关键字是一种常见模式:

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

Animal.prototype.species = '动物';

const dog = new Animal('旺财', 3);
console.log(dog.name);      // 旺财
console.log(dog.species);   // 动物

这里:

  • Animal 是一个构造函数;

  • Animal.prototype 是所有由 Animal 创建的实例共享的对象;

  • 当执行 new Animal() 时,JavaScript 引擎会:

    1. 创建一个空对象;
    2. 将该对象的内部属性 [[Prototype]](可通过 __proto__ 访问)指向 Animal.prototype
    3. 将构造函数内的 this 绑定到这个新对象;
    4. 执行构造函数体;
    5. 返回该对象(除非构造函数显式返回另一个对象)。

因此,dog.species 并不是 dog 自身的属性,而是通过原型链在 Animal.prototype 上找到的。


二、继承的目标:复用父类能力

假设我们希望定义一个 Cat 类,它应具备以下特性:

  1. 拥有 name 和 age 属性(来自 Animal 构造函数);
  2. 能访问 species 属性(来自 Animal.prototype);
  3. 可以拥有自己的属性(如 color)和方法(如 meow)。

要实现这一点,需要完成两个层面的继承:

  • 实例属性继承:通过调用父类构造函数;
  • 原型方法继承:通过建立正确的原型链。

三、实例属性继承:使用 call 或 apply

构造函数中的 this.xxx = value 是在实例对象上直接设置属性。子类若想获得这些属性,必须在自身构造函数中显式调用父类构造函数,并确保 this 指向当前子类实例。

这正是 Function.prototype.callFunction.prototype.apply 的作用。

call 与 apply 的作用

这两个方法允许你临时指定函数执行时的 this 上下文,并立即执行该函数。

  • 语法

    • fn.call(thisArg, arg1, arg2, ...)
    • fn.apply(thisArg, [arg1, arg2, ...])
  • 区别

    • call 接收参数列表;
    • apply 接收参数数组。

应用于继承

function Cat(name, age, color) {
  // 调用 Animal 构造函数,this 指向当前 Cat 实例
  Animal.apply(this, [name, age]);
  // 或 Animal.call(this, name, age);
  
  this.color = color; // Cat 自有属性
}

此时,new Cat('加菲', 2, '橘色') 会正确初始化 nameagecolor

但此时 cat.species 仍为 undefined,因为 Cat.prototype 尚未连接到 Animal.prototype


四、原型方法继承:构建安全的原型链

为了让 Cat 实例能访问 Animal.prototype 上的属性和方法,需设置 Cat.prototype 的原型链。

错误方式一:直接赋值

Cat.prototype = Animal.prototype;

问题:Cat.prototypeAnimal.prototype 指向同一对象。后续对 Cat.prototype 的修改(如添加 eat 方法)会污染 Animal.prototype,破坏封装性。

错误方式二:使用 new Animal()

Cat.prototype = new Animal();

问题:会执行 Animal 构造函数,若其依赖参数(如 name),则可能产生无效或副作用数据;且不必要地创建了父类实例。

正确方式:引入空中介函数

目标:创建一个干净的对象,其 [[Prototype]] 指向 Animal.prototype,但自身无额外属性。

实现:

function extend(Parent, Child) {
  // 1. 定义一个空的构造函数
  var F = function() {};
  
  // 2. 将 F 的 prototype 指向 Parent.prototype
  F.prototype = Parent.prototype;
  
  // 3. 创建 F 的实例作为 Child.prototype
  Child.prototype = new F();
  
  // 4. 修复 constructor 指向
  Child.prototype.constructor = Child;
}
原理解析
  • new F() 创建的对象:

    • 自身无属性(因 F 为空);
    • 其 __proto__ 指向 F.prototype
    • 而 F.prototype = Parent.prototype,因此形成链:
      Child实例 → Child.prototype → Parent.prototype
  • 修改 Child.prototype 不会影响 Parent.prototype,实现隔离。

此模式被称为 “寄生组合式继承” ,是 ES5 时代最推荐的继承方式。


五、完整继承示例

// 父类
function Animal(name, age) {
  this.name = name;
  this.age = age;
}
Animal.prototype.species = '动物';
Animal.prototype.breathe = function() {
  console.log(`${this.name} 在呼吸`);
};

// 子类
function Cat(name, age, color) {
  Animal.apply(this, [name, age]); // 继承实例属性
  this.color = color;
}

// 建立原型链
function extend(Parent, Child) {
  var F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}

extend(Animal, Cat);

// 添加子类特有方法
Cat.prototype.meow = function() {
  console.log('喵~');
};

// 测试
const cat = new Cat('加菲猫', 2, '橘色');
console.log(cat.name);     // 加菲猫
console.log(cat.age);      // 2
console.log(cat.color);    // 橘色
console.log(cat.species);  // 动物
cat.breathe();             // 加菲猫 在呼吸
cat.meow();                // 喵~

六、关于 constructor 属性的说明

每个函数的 prototype 对象默认包含一个 constructor 属性,指向该函数本身:

Cat.prototype.constructor === Cat; // true

但在执行 Child.prototype = new F() 后,Child.prototype.constructor 会变为 F。为保持类型一致性(例如 cat.constructor === Cat),需手动修复:

Child.prototype.constructor = Child;

这有助于调试、类型判断(如 instanceof 虽不受影响,但某些库依赖 constructor)。


七、现代 JavaScript 的继承(ES6+)

ES6 引入了 classextends 语法糖,使继承更直观:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  breathe() {
    console.log(`${this.name} 在呼吸`);
  }
}
Animal.prototype.species = '动物';

class Cat extends Animal {
  constructor(name, age, color) {
    super(name, age); // 等价于 Animal.call(this, name, age)
    this.color = color;
  }
  meow() {
    console.log('喵~');
  }
}

尽管语法更简洁,但其底层机制仍是基于原型链和 [[Prototype]],理解 ES5 的实现有助于深入掌握 JavaScript 本质。


八、总结

JavaScript 的原型继承包含两个核心步骤:

  1. 实例属性继承:在子类构造函数中使用 Parent.apply(this, arguments) 调用父类构造函数,确保实例属性正确初始化;
  2. 原型方法继承:通过“空中介函数”建立 Child.prototype → Parent.prototype 的原型链,实现方法复用且避免污染父类。

掌握这一机制,不仅能写出健壮的继承代码,还能更好地理解现代框架(如 Vue、React)中面向对象的设计思想。原型继承虽初看复杂,但一旦理清思路,便能成为构建可维护、可扩展应用的有力工具。