原型链:[[Prototype]] 查找算法的完整机制
JavaScript 的原型链不是"继承"的语法糖——它是一套精确定义的属性查找算法。每次你写 obj.method() 时,引擎都在执行一个明确的递归查找过程:从对象自身出发,沿 [[Prototype]] 链向上,直到找到属性或到达 null。理解这套算法,才能理解为什么 instanceof 有时会给出反直觉的结果,为什么修改 __proto__ 会导致性能崩溃。
🔹 Level 1 · 你需要知道的
每个对象都有 [[Prototype]]
JavaScript 中每个对象(除了 Object.create(null) 创建的对象)都有一个内部槽 [[Prototype]],指向另一个对象或 null。这个链条就是原型链。
// 查看 [[Prototype]] 的正确方式
const obj = { x: 1 };
Object.getPrototypeOf(obj); // Object.prototype
// 不要用 __proto__(不是规范的一部分,虽然所有主流引擎都支持)
obj.__proto__; // 等价,但不推荐
// 创建指定原型的对象
const child = Object.create(obj);
Object.getPrototypeOf(child) === obj; // true
child.x; // 1 — 从原型 obj 继承的
属性查找规则
查找属性时,引擎遵循以下顺序:
- 先检查对象自身的属性(own properties)
- 找到 → 返回
- 没找到 → 取
[[Prototype]],在上面重复步骤1 [[Prototype]]是null→ 返回undefined
const animal = {
breathe() { return 'breathing'; }
};
const dog = Object.create(animal);
dog.name = 'Rex';
const puppy = Object.create(dog);
// 查找链:puppy → dog → animal → Object.prototype → null
console.log(puppy.name); // 'Rex'(在 dog 上找到)
console.log(puppy.breathe()); // 'breathing'(在 animal 上找到)
console.log(puppy.fly); // undefined(到 null 还没找到)
hasOwnProperty vs in 运算符
const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;
// hasOwnProperty:只检查自有属性
child.hasOwnProperty('own'); // true
child.hasOwnProperty('inherited'); // false
// in 运算符:检查整个原型链
'own' in child; // true
'inherited' in child; // true
'toString' in child; // true — 来自 Object.prototype
// Object.keys:只返回自有可枚举属性
Object.keys(child); // ['own']
// for...in:返回所有可枚举属性(包括原型链)
for (const k in child) console.log(k); // 'own', 'inherited'
实际工作中的建议:
// 安全的 hasOwnProperty 调用(防止对象覆盖了这个方法)
Object.prototype.hasOwnProperty.call(obj, key);
// 或者使用 ES2022 的新 API:
Object.hasOwn(obj, key); // 更简洁,推荐
Object.create() 的使用场景
// 1. 纯数据存储(无原型,避免原型链干扰)
const pureMap = Object.create(null);
pureMap.key = 'value';
// 没有 toString、hasOwnProperty 等继承方法
// 常用于 JSON-like 数据存储或实现 Map 的替代品
// 2. 创建继承关系
const vehicle = {
move() { return `${this.type} is moving at ${this.speed}km/h`; }
};
const car = Object.create(vehicle);
car.type = 'car';
car.speed = 100;
console.log(car.move()); // 'car is moving at 100km/h'
// 3. 实现继承时设置原型(class 的底层机制)
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // 调用父构造函数
this.breed = breed;
}
// 关键:设置 Dog 的原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 指向
Dog.prototype.bark = function() {
return `${this.name} barks`;
};
const rex = new Dog('Rex', 'Labrador');
console.log(rex.speak()); // 'Rex makes a sound'
console.log(rex.bark()); // 'Rex barks'
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
5个常见错误
错误1:不知道 Object.keys 不包含原型属性
function Person(name) { this.name = name; }
Person.prototype.greet = function() {};
const p = new Person('Alice');
Object.keys(p); // ['name'] — 只有自有属性
JSON.stringify(p); // '{"name":"Alice"}' — 同样只有自有可枚举属性
错误2:修改原型影响所有实例
function Cat(name) { this.name = name; }
Cat.prototype.sound = 'meow';
const c1 = new Cat('Tom');
const c2 = new Cat('Jerry');
// 修改原型上的属性影响所有实例
Cat.prototype.sound = 'purr';
console.log(c1.sound); // 'purr'
console.log(c2.sound); // 'purr'
// 修改实例属性只影响该实例(遮蔽原型属性)
c1.sound = 'hiss';
console.log(c1.sound); // 'hiss'(自有属性,遮蔽了原型)
console.log(c2.sound); // 'purr'(仍从原型读取)
错误3:把 class 的 extends 理解为复制
class A {
method() { return 'A'; }
}
class B extends A {}
const b = new B();
b.method(); // 'A'
// 实际上没有复制任何东西——B.prototype 的 [[Prototype]] 指向 A.prototype
Object.getPrototypeOf(B.prototype) === A.prototype; // true
// 修改 A.prototype 仍然影响 B 的实例
A.prototype.method = function() { return 'A modified'; };
b.method(); // 'A modified' — 因为 B 的实例沿原型链找到了修改后的方法
错误4:误用 instanceof
// instanceof 检查的是原型链,不是构造函数身份
function Foo() {}
const f = new Foo();
// 如果修改了原型
Foo.prototype = {};
console.log(f instanceof Foo); // false!— f 的 [[Prototype]] 还是旧的
错误5:Object.create(null) 的对象不能调用原型方法
const safe = Object.create(null);
safe.key = 'value';
safe.toString(); // TypeError: safe.toString is not a function
safe.hasOwnProperty('key'); // TypeError
// 必须显式调用:
Object.prototype.toString.call(safe); // '[object Object]'
Object.hasOwn(safe, 'key'); // true(ES2022)
🔸 Level 2 · 它是怎么运行的
完整的原型链结构图
完整原型链结构(普通对象 → 函数对象):
obj = { x: 1 }
│
│ [[Prototype]]
▼
Object.prototype ← 普通对象的原型链终点
│ constructor: Object
│ toString: function
│ hasOwnProperty: function
│ valueOf: function
│ ...
│
│ [[Prototype]]
▼
null ← 原型链的绝对终点
─────────────────────────────────────────────────────────────
function foo() {}
│
│ [[Prototype]](函数对象的原型链)
▼
Function.prototype ← 所有函数的原型
│ call: function
│ apply: function
│ bind: function
│ ...
│
│ [[Prototype]]
▼
Object.prototype
│
│ [[Prototype]]
▼
null
─────────────────────────────────────────────────────────────
const arr = [1, 2, 3]
│
│ [[Prototype]]
▼
Array.prototype ← 数组实例的原型
│ push, pop, map, filter, ...
│
│ [[Prototype]]
▼
Object.prototype
│
│ [[Prototype]]
▼
null
[[Get]] 操作的完整步骤
当你写 obj.x 时,引擎实际执行的是 obj.[[Get]]('x', obj):
OrdinaryGet(O, P, Receiver) 的执行过程:
┌────────────────────────────────────────────────────────────────────┐
│ │
│ 1. desc ← O.[[GetOwnProperty]](P) │
│ │ │
│ ├── desc 不是 undefined(找到自有属性) │
│ │ ├── IsDataDescriptor(desc) │
│ │ │ └── 返回 desc.[[Value]] │
│ │ └── IsAccessorDescriptor(desc) │
│ │ ├── desc.[[Get]] 是 undefined → 返回 undefined │
│ │ └── 调用 Call(desc.[[Get]], Receiver) │
│ │ └── 返回 getter 的返回值 │
│ │ │
│ └── desc 是 undefined(没有自有属性) │
│ │ │
│ ▼ │
│ 2. parent ← O.[[GetPrototypeOf]]() │
│ │ │
│ ├── parent 是 null → 返回 undefined │
│ │ │
│ └── parent 不是 null │
│ └── 递归调用 parent.[[Get]](P, Receiver) │
│ └── 重复步骤1-2,直到找到或到达 null │
└────────────────────────────────────────────────────────────────────┘
关键细节:Receiver 参数(即 this 的值)在整个递归过程中保持不变。这就是为什么在原型链上找到的方法,this 仍然指向最初调用的那个对象:
const proto = {
getThis() { return this; } // this 是谁?
};
const child = Object.create(proto);
child.getThis() === child; // true — 即使方法在原型上,this 指向 child
V8 的 Inline Cache 优化与原型链深度
V8 使用**内联缓存(Inline Cache, IC)**来加速属性查找。第一次访问某属性时,V8 记录查找路径;下次访问时,如果对象形状(shape/HiddenClass)没变,直接走缓存路径。
V8 属性查找性能层级:
Fast case(最快):
└── 对象有固定形状(HiddenClass),属性在自身 → 直接内存偏移访问
IC hit(快):
└── 形状与缓存匹配,走缓存路径 → 不需要遍历原型链
IC polymorphic(中等):
└── 属性形状有2-4种情况,IC 存储多个路径
IC megamorphic(慢):
└── 形状超过4种,IC 放弃优化,退化为通用查找
原型链深度影响:
┌────────────────────────────────────────┬──────────────────┐
│ 原型链深度 │ 相对查找时间 │
├────────────────────────────────────────┼──────────────────┤
│ 自有属性(depth 0) │ 1x(基准) │
│ depth 1(直接原型) │ ~1.1x │
│ depth 3 │ ~1.5x │
│ depth 6 │ ~2.5x │
│ depth 10+ │ ~4x+(显著退化) │
└────────────────────────────────────────┴──────────────────┘
// 性能测试:自有属性 vs 原型链深度6的属性
function createDeepChain(depth) {
let obj = { value: 'found' };
for (let i = 0; i < depth; i++) {
obj = Object.create(obj);
}
return obj;
}
const shallow = createDeepChain(0); // 自有属性
const deep6 = createDeepChain(6); // 深度6
// 在实际基准测试中,deep6 的访问速度大约是 shallow 的 40-60%
// 具体数字因 V8 版本和对象形状而异
Object.create(null) 的内部机制
const noproto = Object.create(null);
// 等价于创建一个 [[Prototype]] 为 null 的对象
// 内部表示:{ [[Prototype]]: null, ... }
// 有何不同:
Object.getPrototypeOf(noproto); // null
noproto instanceof Object; // false — 没有 Object.prototype 在链上
// 用途:高性能字典/Map 替代品
// 为何比普通对象快?因为不需要检查原型链上的方法是否被遮蔽
const dict = Object.create(null);
dict['toString'] = 'my value'; // 不会与 Object.prototype.toString 冲突
函数的双重原型链
函数对象比普通对象多一个维度——它有两个"原型"相关的属性,经常被混淆:
函数的两个原型相关属性:
function Foo() {}
│
│ [[Prototype]](函数对象本身的原型)
▼
Function.prototype(所有函数都在这条链上)
│
│ [[Prototype]]
▼
Object.prototype
│
▼
null
Foo.prototype(new Foo() 创建的实例的原型)
│ ↑
│ 这是不同的东西!
│
└── Foo.prototype.[[Prototype]] === Object.prototype
function Foo() {}
// [[Prototype]]:Foo 函数对象的原型(Foo 是 Function 的实例)
Object.getPrototypeOf(Foo) === Function.prototype; // true
// Foo.prototype:用 new Foo() 时,实例的 [[Prototype]] 被设置为这个
const instance = new Foo();
Object.getPrototypeOf(instance) === Foo.prototype; // true
🔺 Level 3 · 规范怎么定义的
规范 §10.1.8 [[GetPrototypeOf]]
规范原文(§10.1.8):
10.1.8 OrdinaryGetPrototypeOf ( O )
The abstract operation OrdinaryGetPrototypeOf takes argument O (an Object) and returns an Object or null. It performs the following steps when called:
- Return O.[[Prototype]].
这是最简单的规范操作之一——直接返回内部槽的值。
OrdinaryGet 的完整查找算法
规范 §10.1.8 OrdinaryGet(O, P, Receiver)——这是原型链查找的核心算法:
规范文本(简化版,去掉 spec notation 保留关键逻辑):
OrdinaryGet(O, P, Receiver):
1. desc ← OrdinaryGetOwnProperty(O, P)
(查找 O 自身是否有属性 P)
2. 如果 desc 是 undefined:
a. parent ← OrdinaryGetPrototypeOf(O)
(获取 O 的 [[Prototype]])
b. 如果 parent 是 null:
返回 undefined
c. 返回 parent.[[Get]](P, Receiver)
(递归:在 parent 上查找,Receiver 保持不变)
3. 如果 IsDataDescriptor(desc):
返回 desc.[[Value]]
4. 断言:IsAccessorDescriptor(desc)
getter ← desc.[[Get]]
5. 如果 getter 是 undefined:
返回 undefined
6. 返回 Call(getter, Receiver)
(调用 getter,this = Receiver)
注意 Receiver 的作用:Receiver 是属性访问表达式的 this 值。即使属性在原型链的深处,this 仍然是最初的那个对象。这就是多态方法能正确工作的原因:
class Shape {
area() { return this.width * this.height; } // this 指向具体实例
}
class Rectangle extends Shape {
constructor(w, h) {
super();
this.width = w;
this.height = h;
}
}
const r = new Rectangle(4, 5);
r.area(); // 20 — area 在 Shape.prototype 上,但 this 是 r
[[GetPrototypeOf]] 的规范约束
规范(§10.1.8)对 [[GetPrototypeOf]] 的不变量(Invariants):
6.1.7.3 Invariants of the Essential Internal Methods
[[GetPrototypeOf]] ()
- The Type of the return value must be either Object or Null.
- If O is not extensible, [[GetPrototypeOf]] must always return the same value.
这个不变量解释了为什么 Proxy 代理不能在不可扩展对象上撒谎它的原型——违反不变量会在 Proxy 层抛出 TypeError(见 ch15)。
instanceof 的规范实现
x instanceof F 不是检查 x 是否由 F 构造,而是检查原型链:
规范 §13.10.2 InstanceofOperator(V, target):
1. 如果 target 不是对象 → TypeError
2. instOfHandler ← GetMethod(target, @@hasInstance)
(检查 target[Symbol.hasInstance] 是否存在)
3. 如果 instOfHandler 不是 undefined:
返回 ToBoolean(Call(instOfHandler, target, [V]))
(允许自定义 instanceof 行为)
4. 如果 IsCallable(target) 是 false → TypeError
5. 返回 OrdinaryHasInstance(target, V)
OrdinaryHasInstance(C, O):
1. 如果 IsCallable(C) 是 false → false
2. 如果 C 有 [[BoundTargetFunction]] → 用绑定目标函数递归
3. 如果 Type(O) 不是 Object → false
4. P ← C.prototype
如果 P 不是对象 → TypeError
5. 循环:
O ← O.[[GetPrototypeOf]]()
如果 O 是 null → false
如果 SameValue(O, P) 是 true → true
(继续循环,沿原型链向上)
// 验证:instanceof 检查原型链,不是构造函数身份
function Foo() {}
const f = new Foo();
// 修改 Foo.prototype
Foo.prototype = {}; // 新对象
console.log(f instanceof Foo); // false!
// 因为:
// f.[[Prototype]] 还是旧的 Foo.prototype(一个对象)
// 但新 Foo.prototype 是一个全新的对象
// SameValue 比较失败
// 自定义 instanceof 行为:
class EvenNumber {
static [Symbol.hasInstance](num) {
return Number.isInteger(num) && num % 2 === 0;
}
}
console.log(2 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
console.log(4.5 instanceof EvenNumber); // false
💎 Level 4 · 边界与陷阱
陷阱1:Function.prototype 本身是一个对象
这是 JavaScript 原型体系最令人困惑的环形引用:
// Function.prototype 是函数,但同时也是对象
typeof Function.prototype; // 'function' — 可以调用
Function.prototype(); // undefined(返回 undefined,不报错)
// 它的 [[Prototype]] 是 Object.prototype(不是 Function.prototype 自身)
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
// 完整的互相依赖关系:
Object.getPrototypeOf(Function) === Function.prototype; // true(Function 是自己的实例)
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object) === Function.prototype; // true(Object 也是函数)
Object.getPrototypeOf(Object.prototype) === null; // true(链的终点)
用图来表示这个"鸡生蛋蛋生鸡"的关系:
原型链的循环引用关系图:
Function ──[[Prototype]]──▶ Function.prototype ──[[Prototype]]──▶ Object.prototype ──▶ null
▲ │
│ │ (Function.prototype 是 Object 的实例)
│ ▼
Object ──[[Prototype]]──▶ Function.prototype(同一个对象)
│
│ Object.prototype ──[[Prototype]]──▶ null
// 验证所有关系:
console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true(Function 是自己的实例)
// 为什么 Function instanceof Object 是 true?
// Function.[[Prototype]] = Function.prototype
// Function.prototype.[[Prototype]] = Object.prototype ← 这里命中了
// 所以 Function 在 Object.prototype 的原型链上 → true
// 为什么 Object instanceof Function 是 true?
// Object.[[Prototype]] = Function.prototype ← 这里命中了
// → true
陷阱2:Object.prototype.proto === null
// Object.prototype 是原型链的终点
Object.getPrototypeOf(Object.prototype); // null
// __proto__ 在这里确实是 null
Object.prototype.__proto__; // null
// 但注意:Object.prototype.__proto__ 是一个访问器属性
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {
// get: [Function: get __proto__],
// set: [Function: set __proto__],
// enumerable: false,
// configurable: true
// }
// __proto__ 的 getter 等价于 Object.getPrototypeOf
// __proto__ 的 setter 等价于 Object.setPrototypeOf
陷阱3:instanceof 的互相引用悖论推导
这道题经常出现在高级面试中:
Function instanceof Object // true 还是 false?
Object instanceof Function // true 还是 false?
完整推导:
// Function instanceof Object 的推导:
// 问:Function.[[Prototype]] 链上是否有 Object.prototype?
//
// Function.[[Prototype]] = Function.prototype
// Function.prototype.[[Prototype]] = Object.prototype ← 命中!
// 结论:Function instanceof Object === true
// Object instanceof Function 的推导:
// 问:Object.[[Prototype]] 链上是否有 Function.prototype?
//
// Object.[[Prototype]] = Function.prototype ← 直接命中!
// (因为 Object 是一个函数,所有函数的 [[Prototype]] 都是 Function.prototype)
// 结论:Object instanceof Function === true
这两个都是 true 的原因:JavaScript 的原型体系是自举的(bootstrapped)——Function 和 Object 在引擎初始化时被特殊创建,形成了这个循环依赖关系。普通用户代码无法创建这样的循环。
陷阱4:直接修改 proto 的性能惩罚
const obj = { x: 1 };
// 这行代码会造成严重的性能问题:
obj.__proto__ = { y: 2 };
// 或者:
Object.setPrototypeOf(obj, { y: 2 });
为什么代价高昂?
V8 为每个对象维护一个隐藏类(HiddenClass / Shape),用于快速确定属性的内存偏移。当你修改一个对象的 [[Prototype]] 时:
修改 [[Prototype]] 的代价:
1. 使当前对象的 HiddenClass 失效
└── 所有依赖这个 HiddenClass 的 Inline Cache 全部失效
2. 如果这个对象是某个函数内的局部变量,
V8 可能使整个函数的优化代码(JIT compiled code)失效(deoptimize)
3. 影响范围:不只是这一行代码,而是所有曾经访问过这个对象的代码路径
实际测量(V8 8.x):
正常属性访问:~1ns
修改 [[Prototype]] 后的属性访问:~10-50ns(首次,等待重新优化)
在热路径上频繁修改 [[Prototype]]:可能导致 >10x 的性能下降
// 错误模式:在循环中修改原型
function process(items) {
items.forEach(item => {
Object.setPrototypeOf(item, specialProto); // 每次都使 IC 失效!
});
}
// 正确模式:在创建时就设置好原型
function createItem(data) {
return Object.assign(Object.create(specialProto), data);
}
Node.js 文档专门警告过这一点:
"Changing the prototype of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-reaching, and are not limited to simply the time spent in
obj.__proto__ = ...statement."
陷阱5:Object.create(null) 在 JSON.stringify 中的表现
const noproto = Object.create(null);
noproto.name = 'Alice';
noproto.age = 30;
// JSON.stringify 实际上不依赖 Object.prototype
JSON.stringify(noproto); // '{"name":"Alice","age":30}' — 正常工作!
// 为什么?JSON.stringify 内部使用 [[OwnPropertyKeys]] 和 [[Get]],
// 这些是内部方法,不依赖原型链
// 但是 toJSON 方法依赖原型链:
noproto.toJSON = function() { return { custom: true }; };
JSON.stringify(noproto); // '{"custom":true}' — toJSON 是自有方法,能被调用
// 会出问题的场景:调用任何继承自 Object.prototype 的方法
noproto.toString(); // TypeError — 没有 toString 方法
noproto.hasOwnProperty('name'); // TypeError
// 检测 Object.create(null) 对象:
Object.getPrototypeOf(noproto) === null; // true
// 在类型检查时需要特别处理:
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false;
const proto = Object.getPrototypeOf(obj);
return proto === null || proto === Object.prototype;
}
真实 bug 案例:用 Object.create(null) 作为缓存,然后用 if (cache[key]) 检查缓存命中,这本身没问题。但如果代码后来加了 Object.keys(cache).forEach(...) 或者调用了 cache.hasOwnProperty(key),就会崩溃。正确的做法是在这些地方用 Object.hasOwn(cache, key)(ES2022)或者 Object.prototype.hasOwnProperty.call(cache, key)。
原型链与内存泄漏
原型链本身不会引起内存泄漏,但某些模式会:
// 危险模式:在原型上存储大量数据
function createHeavyProto() {
const largeData = new Array(1000000).fill('data');
return { largeData };
}
const heavyProto = createHeavyProto();
// 所有以 heavyProto 为原型的对象都持有这个引用
const obj1 = Object.create(heavyProto);
const obj2 = Object.create(heavyProto);
// largeData 不会被 GC,直到 heavyProto 自身也没有引用
// 正确模式:共享的是方法,不是大数据
const proto = {
// 方法很小,没问题
greet() { return `Hello, ${this.name}`; }
};
// 数据存在实例上
const obj = Object.create(proto);
obj.name = 'Alice';
本章小结
-
原型链是一个精确的递归查找算法:
[[Get]]从当前对象出发,沿[[Prototype]]逐级向上,直到找到属性或到达null。Receiver(this值)在整个递归中保持为最初调用的对象。 -
hasOwnProperty检查自有属性,in检查整个链:日常代码推荐用Object.hasOwn(obj, key)(ES2022)替代obj.hasOwnProperty(key),因为后者可能被覆盖。 -
修改
[[Prototype]]代价极高:V8 会使所有相关的 Inline Cache 和 JIT 优化代码失效,应该在创建对象时就设置好原型,而不是事后修改。 -
instanceof检查的是原型链,不是构造函数身份:修改F.prototype后,已有实例的instanceof F结果会改变;Function instanceof Object和Object instanceof Function都是true,这是引擎自举造成的循环依赖。 -
Object.create(null)创建无原型对象:适合用作纯数据字典,避免原型链干扰,但不能调用任何继承自Object.prototype的方法,需要全部显式调用。