为什么原型链是 JavaScript 的基石
JavaScript 是一门基于原型(prototype-based) 的语言,不同于 Java/C++ 的基于类(class-based)继承。即使 ES2015 引入了 class 关键字,底层仍然是原型链机制。理解原型链,才能真正理解 instanceof、Object.create、class extends 的行为。
一、原型基础
1.1 两个关键属性
| 属性 | 归属 | 含义 |
|---|---|---|
prototype |
函数对象 | 该构造函数创建的实例的原型对象 |
[[Prototype]] / __proto__ |
所有对象 | 指向该对象的原型(即继承来源) |
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const alice = new Person('Alice');
// 关系链
alice.__proto__ === Person.prototype; // true
Person.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true — 原型链终点
注意:
__proto__是非标准的访问器属性(虽然已被大多数引擎实现)。推荐使用Object.getPrototypeOf(obj)和Object.setPrototypeOf(obj, proto)。
1.2 [[Prototype]] 查找机制
当访问对象的属性时,引擎遵循以下流程:
- 在对象自身属性(own properties)中查找
- 未找到 → 沿
[[Prototype]]链向上查找 - 到达
Object.prototype仍未找到 → 返回undefined
alice.greet(); // 在 Person.prototype 上找到
alice.toString(); // 在 Object.prototype 上找到
alice.foo; // 沿链到 null,返回 undefined
属性遮蔽(Property Shadowing):如果对象自身和原型链上同时存在同名属性,自身属性优先(遮蔽原型上的)。
alice.greet = function () { return 'Override!'; };
alice.greet(); // 'Override!' — 自身属性遮蔽了原型方法
delete alice.greet;
alice.greet(); // "Hi, I'm Alice" — 恢复从原型链查找
1.3 hasOwnProperty 与 in 的区别
alice.hasOwnProperty('name'); // true — 自身属性
alice.hasOwnProperty('greet'); // false — 来自原型
'greet' in alice; // true — in 会遍历原型链
// ES2022 推荐:Object.hasOwn()
Object.hasOwn(alice, 'name'); // true
二、构造函数与 new 运算符
2.1 new 的四步过程
当执行 new Foo(args) 时,引擎内部执行:
- 创建一个新的空对象
obj - 将
obj.__proto__设为Foo.prototype - 以
obj作为this执行Foo(args) - 如果
Foo返回了一个对象,则new表达式的结果就是该对象;否则返回obj
// 手动模拟 new
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // 步骤 1+2
const result = Constructor.apply(obj, args); // 步骤 3
return result instanceof Object ? result : obj; // 步骤 4
}
2.2 constructor 属性
Person.prototype.constructor === Person; // true
alice.constructor === Person; // true(沿原型链找到)
如果重写了 prototype 对象,需要手动恢复 constructor:
Person.prototype = {
greet() { /* ... */ }
// ❌ constructor 丢失
};
// 修复:
Person.prototype = {
constructor: Person,
greet() { /* ... */ }
};
三、继承模式演进
3.1 原型链继承
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return `${this.name} speaks`; };
function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 建立原型链
Dog.prototype.constructor = Dog; // 修复 constructor
Dog.prototype.bark = function () { return 'Woof!'; };
这是 ES5 时代的寄生组合式继承(Parasitic Combination Inheritance),被认为是最理想的经典继承方案。
3.2 各继承模式对比
| 模式 | 核心思路 | 缺陷 |
|---|---|---|
| 原型链继承 | Child.prototype = new Parent() |
引用类型属性共享;无法向父构造函数传参 |
| 借用构造函数 | Parent.call(this) |
方法无法复用(每次实例化都创建) |
| 组合继承 | 原型链 + 借用构造函数 | 父构造函数被调用两次 |
| 寄生组合式继承 | Object.create(Parent.prototype) + Parent.call(this) |
✅ 最佳方案 |
Object.create |
纯原型委托 | 没有构造函数的初始化逻辑 |
3.3 Object.create 深入
// Object.create 的简化实现
function objectCreate(proto) {
function F() {}
F.prototype = proto;
return new F();
}
// 创建一个没有原型的"纯净"对象
const dict = Object.create(null);
// dict 没有 toString、hasOwnProperty 等方法
// 适合做纯粹的键值存储
四、ES6 class — 语法糖的本质
4.1 class 到 prototype 的映射
class Animal {
constructor(name) {
this.name = name; // → 实例属性
}
speak() { // → Animal.prototype.speak
return `${this.name} speaks`;
}
static create(name) { // → Animal.create(构造函数本身的属性)
return new Animal(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // → Animal.call(this, name)
this.breed = breed;
}
bark() { // → Dog.prototype.bark
return 'Woof!';
}
}
等价关系:
typeof Animal; // 'function'
Dog.prototype.__proto__ === Animal.prototype; // true — 实例方法继承
Dog.__proto__ === Animal; // true — 静态方法继承
4.2 class 与传统函数构造器的差异
虽然 class 是语法糖,但有一些重要的行为差异:
| 特性 | class |
传统函数构造器 |
|---|---|---|
必须使用 new 调用 |
✅ 否则 TypeError | ❌ 可直接调用 |
| 方法不可枚举 | ✅ enumerable: false |
❌ 默认可枚举 |
| 严格模式 | ✅ 自动启用 | ❌ 需要手动声明 |
| 存在 TDZ | ✅ 不会被提升 | ❌ 函数声明会提升 |
super 关键字 |
✅ 支持 | ❌ 需要手动 Parent.call(this) |
4.3 super 的两种用法
class Dog extends Animal {
constructor(name, breed) {
// super() 作为函数调用 — 必须在使用 this 之前调用
super(name);
this.breed = breed;
}
speak() {
// super.method() — 访问父类原型上的方法
return `${super.speak()} — and barks!`;
}
}
规范细节:在 extends 的子类构造函数中,this 在 super() 调用之前是未初始化的(处于 TDZ 状态),提前访问会抛出 ReferenceError。
五、instanceof 与类型检测
5.1 instanceof 的原理
a instanceof B 实质上是检查 B.prototype 是否存在于 a 的原型链上。
// 简化实现
function myInstanceOf(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
5.2 Symbol.hasInstance 自定义
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
4 instanceof EvenNumber; // true
3 instanceof EvenNumber; // false
六、面试高频题型
题型 1:画出原型链关系图
alice → Person.prototype → Object.prototype → null
↑ constructor: Person ↑ constructor: Object
题型 2:手写 new 操作符
见 2.1 节的 myNew 实现。
题型 3:手写 instanceof
见 5.1 节的 myInstanceOf 实现。
题型 4:class 的本质是什么
"ES6 的
class是语法糖,底层仍是 prototype 机制。class定义的方法挂在.prototype上,extends通过Object.setPrototypeOf建立两条原型链(实例链和构造函数链),super()等价于Parent.call(this)但有更严格的 TDZ 约束。"
题型 5:如何实现多继承(Mixins)
JavaScript 不支持多继承,但可以用 Mixin 模式:
const Serializable = (Base) => class extends Base {
serialize() {
return JSON.stringify(this);
}
};
const Validatable = (Base) => class extends Base {
validate() {
return Object.keys(this).every(key => this[key] != null);
}
};
class User extends Serializable(Validatable(Animal)) {
// 同时获得 serialize() 和 validate() 方法
}
七、易错点与最佳实践
7.1 修改内置原型(Monkey Patching)
// ❌ 永远不要这样做
Array.prototype.sum = function () {
return this.reduce((a, b) => a + b, 0);
};
修改内置原型可能导致:命名冲突、不可预测的行为、与未来 ECMAScript 标准冲突。
7.2 for...in 会遍历原型链
for (const key in alice) {
console.log(key); // name, greet — 包括原型上的属性
}
// 过滤方式
for (const key in alice) {
if (Object.hasOwn(alice, key)) {
console.log(key); // 只有 name
}
}
// 更好的选择:Object.keys() 只返回自身可枚举属性
Object.keys(alice); // ['name']
八、与其他主题的关联
| 关联主题 | 关系 |
|---|---|
| this-binding | new 绑定的 this 指向新创建的实例 |
| proxy-reflect | Proxy 可以拦截 [[GetPrototypeOf]] 等内部操作 |
| design-patterns-js | 工厂模式、策略模式等常依赖原型链实现 |
| es6-features | class 语法、Symbol.hasInstance 均为 ES2015+ 特性 |
参考资料
- Kyle Simpson, You Don't Know JS: this & Object Prototypes (2nd Edition) — 对原型委托最深入的讨论
- MDN Web Docs — Inheritance and the prototype chain
- ECMA-262 — OrdinaryGetPrototypeOf
- Nicholas C. Zakas, Professional JavaScript for Web Developers — 继承模式章节
- Axel Rauschmayer, Speaking JavaScript — 原型链可视化讲解