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 引擎会:- 创建一个空对象;
- 将该对象的内部属性
[[Prototype]](可通过__proto__访问)指向Animal.prototype; - 将构造函数内的
this绑定到这个新对象; - 执行构造函数体;
- 返回该对象(除非构造函数显式返回另一个对象)。
因此,dog.species 并不是 dog 自身的属性,而是通过原型链在 Animal.prototype 上找到的。
二、继承的目标:复用父类能力
假设我们希望定义一个 Cat 类,它应具备以下特性:
- 拥有
name和age属性(来自Animal构造函数); - 能访问
species属性(来自Animal.prototype); - 可以拥有自己的属性(如
color)和方法(如meow)。
要实现这一点,需要完成两个层面的继承:
- 实例属性继承:通过调用父类构造函数;
- 原型方法继承:通过建立正确的原型链。
三、实例属性继承:使用 call 或 apply
构造函数中的 this.xxx = value 是在实例对象上直接设置属性。子类若想获得这些属性,必须在自身构造函数中显式调用父类构造函数,并确保 this 指向当前子类实例。
这正是 Function.prototype.call 和 Function.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, '橘色') 会正确初始化 name、age 和 color。
但此时 cat.species 仍为 undefined,因为 Cat.prototype 尚未连接到 Animal.prototype。
四、原型方法继承:构建安全的原型链
为了让 Cat 实例能访问 Animal.prototype 上的属性和方法,需设置 Cat.prototype 的原型链。
错误方式一:直接赋值
Cat.prototype = Animal.prototype;
问题:Cat.prototype 与 Animal.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 引入了 class 和 extends 语法糖,使继承更直观:
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 的原型继承包含两个核心步骤:
- 实例属性继承:在子类构造函数中使用
Parent.apply(this, arguments)调用父类构造函数,确保实例属性正确初始化; - 原型方法继承:通过“空中介函数”建立
Child.prototype → Parent.prototype的原型链,实现方法复用且避免污染父类。
掌握这一机制,不仅能写出健壮的继承代码,还能更好地理解现代框架(如 Vue、React)中面向对象的设计思想。原型继承虽初看复杂,但一旦理清思路,便能成为构建可维护、可扩展应用的有力工具。