🧬JavaScript 是一门基于原型(Prototype-based)的动态语言,其继承机制与传统的类式(Class-based)语言(如 Java、C++)有本质区别。本文将系统性地讲解 JavaScript 中 原型继承 的核心原理、多种实现方式、对象创建方法、call/apply 的作用、组合继承优化技巧、以及现代 ES6 class 语法的本质,并结合实际代码深入剖析每一个细节。
🔗 一、原型与原型链:JavaScript 继承的基石
📌 构造函数、原型、实例三者关系
在 JavaScript 中,每个函数都有一个 prototype 属性,它指向一个对象(称为原型对象)。当我们使用 new 关键字调用构造函数时,会创建一个新对象,该对象的内部属性 [[Prototype]](可通过 __proto__ 访问)会指向构造函数的 prototype。
function Cat() {}
Cat.prototype.species = '猫科动物';
const cat = new Cat();
console.log(cat.__proto__ === Cat.prototype); // true
此时:
Cat是构造函数;Cat.prototype是所有Cat实例共享的原型对象;cat是实例,其__proto__指向Cat.prototype。
🔍 属性查找机制:遮蔽(Shadowing)
当访问 cat.species 时,JavaScript 引擎按以下顺序查找:
- 先查实例自身:
cat是否有species属性? - 若无,则沿
__proto__向上查找:即Cat.prototype.species; - 继续向上:
Cat.prototype.__proto__→Object.prototype; - 最终到
null,查找失败。
console.log(cat.species); // '猫科动物'(来自原型)
cat.species = 'hello'; // 在实例上定义同名属性
console.log(cat.species); // 'hello'(遮蔽了原型属性)
delete cat.species;
console.log(cat.species); // '猫科动物'(恢复原型值)
✅ 关键点:实例属性优先于原型属性,这就是“属性遮蔽”。
🌐 原型链可视化
cat (实例)
└─ __proto__ → Cat.prototype
└─ __proto__ → Object.prototype
└─ __proto__ → null
验证:
console.log(cat.constructor === Cat); // true
console.log(Cat.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
⚙️ 二、继承的本质:对象委托(Object Delegation)
JavaScript 并非“类继承”,而是对象直接从其他对象继承——这称为委托。
当你调用 cat.say(),而 cat 自身没有 say 方法时,引擎会委托给 Cat.prototype 去执行。
Cat.prototype.say = function() { return '喵喵喵'; };
const cat1 = new Cat(), cat2 = new Cat();
console.log(cat1.say === cat2.say); // true(共享同一个函数)
💡 这就是方法复用的核心优势:节省内存,提高性能。
🧱 三、继承的多种实现方式
1️⃣ 原型链继承(Prototype Chain Inheritance)
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() { return this.property; };
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 关键:子类原型 = 父类实例
SubType.prototype.getSubValue = function() { return this.subproperty; };
✅ 优点:简单直观。
❌ 缺点:
- 引用类型属性被所有实例共享(如
colors: []); - 无法向父构造函数传参。
2️⃣ 构造函数继承(Constructor Stealing / Classical Inheritance)
利用 call 或 apply 在子类中调用父类构造函数:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function SubType(name, age) {
SuperType.call(this, name); // ✅ 绑定 this,传递参数
this.age = age;
}
🔁 call vs apply 详解
| 方法 | 语法 | 参数形式 | 适用场景 |
|---|---|---|---|
call | func.call(thisArg, arg1, arg2, ...) | 逐个参数 | 参数数量固定 |
apply | func.apply(thisArg, [arg1, arg2, ...]) | 数组或 arguments | 参数动态、已存在数组 |
// 示例:借用方法
greet.call(person1, '你好');
Math.max.apply(null, [1,2,3,4,5]); // 推荐方式
// 继承中两者等价
SuperType.call(this, name);
SuperType.apply(this, [name]);
✅ 优点:
- 可传参;
- 避免引用类型共享(每个实例独立副本)。
❌ 缺点:
- 方法定义在构造函数内 → 无法复用;
- 无法继承父类原型上的方法。
3️⃣ 组合继承(Combination Inheritance)⭐ 最常用
结合前两种方式:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() { return this.name; };
function SubType(name, age) {
SuperType.call(this, name); // 继承属性(每个实例独立)
this.age = age;
}
SubType.prototype = new SuperType(); // 继承方法(共享)
SubType.prototype.constructor = SubType; // 修复 constructor
SubType.prototype.sayAge = function() { return this.age; };
✅ 优点:兼具属性独立 + 方法复用。
❌ 缺点:父类构造函数被调用两次(一次 new SuperType(),一次 SuperType.call)。
4️⃣ 利用空对象作为中介(寄生组合继承)✨ 优化版
为避免父构造函数被调用两次,引入空函数中介:
function extend(Child, Parent) {
const F = function() {}; // 空函数
F.prototype = Parent.prototype; // 中介原型 = 父类原型
Child.prototype = new F(); // 子类原型 = 空实例
Child.prototype.constructor = Child; // 修复 constructor
}
// 使用
extend(Cat, Animal);
🧠 原理:
new F()不会执行Parent构造函数,只建立原型链接。
这是 最高效的原型链继承方式,也是许多库(如早期 jQuery)采用的模式。
🧪 四、new Object() 与 new function() 创建对象的区别
🔹 new Object()
var obj1 = new Object(); // {}
var obj2 = new Object("hello"); // String 对象
- 原型:
Object.prototype - 初始化能力弱
- 适合创建简单空对象
🔸 new function()
var user = new function(username) {
if (!username) throw Error("用户名不能为空");
this.username = username;
var privateVar = "secret"; // 私有变量(闭包)
this.check = function(pwd) { return pwd === privateVar; };
};
- 原型:自定义函数的
prototype - 支持复杂初始化、私有变量、方法定义
- 可用于对象工厂
| 特性 | new Object() | new function() |
|---|---|---|
| 原型 | Object.prototype | 自定义 prototype |
| 私有属性 | ❌ | ✅(闭包) |
| 方法复用 | ❌ | ✅(通过原型) |
| 灵活性 | 低 | 高 |
💡 在继承中,
new function()(空函数)是实现中介继承的关键。
🧩 五、ES6 class 语法:语法糖,本质仍是原型
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name}`;
}
}
class Student extends Person {
constructor(name, grade) {
super(name); // 必须调用,等价于 Person.call(this, name)
this.grade = grade;
}
study() {
return `${this.name} is studying...`;
}
}
⚠️ 注意:
extends本质仍是基于原型的继承;super()必须在子类构造函数中调用;- 静态方法和属性也可继承。
🛠 六、最佳实践与高级技巧
✅ 组合优于继承(Composition over Inheritance)
const canFly = { fly() { console.log('Flying...'); } };
const canSwim = { swim() { console.log('Swimming...'); } };
const duck = Object.assign({}, canFly, canSwim);
更灵活,避免继承层级过深。
✅ 使用标准 API 操作原型
// 获取原型
Object.getPrototypeOf(obj);
// 设置原型(谨慎!影响性能)
Object.setPrototypeOf(obj, newProto);
// 创建对象并指定原型
Object.create(proto);
避免使用非标准的
__proto__。
✅ 重写原型时修复 constructor
Person.prototype = {
constructor: Person, // 手动设置
method1() {},
method2() {}
};
否则 instance.constructor 会指向 Object。
⚠️ 七、常见陷阱与注意事项
1. 原型上不要放引用类型
// ❌ 错误
Person.prototype.skills = ['JS', 'HTML']; // 所有实例共享!
// ✅ 正确
function Person() {
this.skills = ['JS', 'HTML']; // 每个实例独立
}
2. 原型链过深影响性能
频繁访问的属性应直接定义在实例上,避免长链查找。
3. instanceof 原理
myDog instanceof Dog; // 检查 Dog.prototype 是否在 myDog 的原型链上
🧪 八、综合示例:完整的动物继承体系
// 父类
function Animal(name) {
this.name = name;
this.alive = true;
}
Animal.prototype = {
constructor: Animal,
eat() { console.log(`${this.name} 在进食`); },
sleep() { console.log(`${this.name} 在睡觉`); }
};
// 子类
function Dog(name, breed) {
Animal.call(this, name); // 构造函数继承
this.breed = breed;
}
// 原型链继承(空对象中介)
const F = function() {};
F.prototype = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;
// 扩展方法
Dog.prototype.bark = function() {
console.log(`${this.name} 汪汪叫`);
};
// 测试
const myDog = new Dog('小黑', '拉布拉多');
myDog.eat(); // 继承自动物
myDog.bark(); // 狗特有
console.log(myDog instanceof Animal); // true
🌈 总结
JavaScript 的继承机制围绕 原型链 和 对象委托 展开,核心概念包括:
- 构造函数:创建对象的模板;
- prototype:共享属性和方法的容器;
- proto:实例指向原型的链接;
- call/apply:实现构造函数继承的关键;
- 组合继承 + 空对象中介:最优实践;
- ES6 class:语法糖,底层仍是原型。
理解这些机制,不仅能写出健壮的继承代码,还能深入掌握 JavaScript 的面向对象哲学——万物皆对象,继承即委托。
🧠 记住:JavaScript 不是“模拟类”,而是原生支持对象间直接继承。拥抱原型,而非对抗它。