第 13 章

原型链:[[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 继承的

属性查找规则

查找属性时,引擎遵循以下顺序:

  1. 先检查对象自身的属性(own properties)
  2. 找到 → 返回
  3. 没找到 → 取 [[Prototype]],在上面重复步骤1
  4. [[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:

  1. 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)——FunctionObject 在引擎初始化时被特殊创建,形成了这个循环依赖关系。普通用户代码无法创建这样的循环。

陷阱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';

本章小结

  1. 原型链是一个精确的递归查找算法[[Get]] 从当前对象出发,沿 [[Prototype]] 逐级向上,直到找到属性或到达 null。Receiver(this 值)在整个递归中保持为最初调用的对象。

  2. hasOwnProperty 检查自有属性,in 检查整个链:日常代码推荐用 Object.hasOwn(obj, key)(ES2022)替代 obj.hasOwnProperty(key),因为后者可能被覆盖。

  3. 修改 [[Prototype]] 代价极高:V8 会使所有相关的 Inline Cache 和 JIT 优化代码失效,应该在创建对象时就设置好原型,而不是事后修改。

  4. instanceof 检查的是原型链,不是构造函数身份:修改 F.prototype 后,已有实例的 instanceof F 结果会改变;Function instanceof ObjectObject instanceof Function 都是 true,这是引擎自举造成的循环依赖。

  5. Object.create(null) 创建无原型对象:适合用作纯数据字典,避免原型链干扰,但不能调用任何继承自 Object.prototype 的方法,需要全部显式调用。

本章评分
4.7  / 5  (24 评分)

💬 留言讨论