原型链
一、什么是原型链
JavaScript 是一门基于 原型(Prototype) 的语言,没有传统意义上的"类继承",对象之间的继承是通过 原型链(Prototype Chain) 实现的。
每个 JavaScript 对象(除 null 外)在创建时都会关联另一个对象,这个被关联的对象就叫做它的 原型(prototype)。当访问一个对象的属性或方法时,如果对象自身没有,就会沿着这条原型链向上查找,直到找到该属性,或到达链的终点 null。
一句话概括:原型链 = 对象通过
[[Prototype]]一层一层串起来的查找链路。
obj ──[[Prototype]]──▶ Constructor.prototype ──[[Prototype]]──▶ Object.prototype ──[[Prototype]]──▶ null二、三个核心概念辨析
理解原型链,必须先分清三个极易混淆的概念:
| 概念 | 归属 | 含义 | 访问方式 |
|---|---|---|---|
[[Prototype]] | 所有对象(内部插槽) | 对象隐式持有的原型引用 | Object.getPrototypeOf(obj) |
__proto__ | 所有对象(访问器) | [[Prototype]] 的非标准但通用的访问器 | obj.__proto__ |
prototype | 仅函数对象 才有 | 函数作为构造器时,用 new 创建的实例的原型 | Func.prototype |
关键关系:
function Foo() {}
const f = new Foo();
Object.getPrototypeOf(f) === Foo.prototype; // true
f.__proto__ === Foo.prototype; // true
Foo.prototype.constructor === Foo; // true记忆口诀:实例的 __proto__ 指向构造函数的 prototype。
三、原型链的属性查找过程
当读取 obj.x 时,JS 引擎会执行:
- 在
obj自身属性中查找x,找到则返回; - 找不到则沿
obj.[[Prototype]]向上查找; - 重复第 2 步,直到找到
x或某一层的[[Prototype]]为null; - 全程未找到则返回
undefined(注意:不是抛错)。
const o = {
a: 1,
b: 2,
__proto__: {
b: 3,
c: 4,
},
};
// 链路:{a:1,b:2} ──▶ {b:3,c:4} ──▶ Object.prototype ──▶ null
o.a; // 1 ← 自有
o.b; // 2 ← 自有,"遮蔽"了原型上的 b
o.c; // 4 ← 原型链上找到
o.d; // undefined ← 整条链都没有属性遮蔽(Shadowing)
写操作和读操作不同:赋值只会作用在对象自身,不会修改原型上的属性,从而形成"遮蔽"。
const parent = { value: 2 };
const child = { __proto__: parent };
child.value; // 2(继承自 parent)
child.value = 4; // 在 child 自身创建一个新属性
child.value; // 4
parent.value; // 2(原型未受影响)四、原型链的终点
绝大多数对象的链最终指向 Object.prototype,再往上就是 null:
Object.getPrototypeOf(Object.prototype); // null例外:用 Object.create(null) 创建的对象 没有原型,也就没有 hasOwnProperty、toString 等方法,常用于实现纯净的"字典对象"。
const dict = Object.create(null);
dict.hasOwnProperty; // undefined五、new 运算符与原型链的建立
new Foo(args) 的内部步骤大致如下:
- 创建一个新对象
obj = {}; - 将
obj.[[Prototype]]指向Foo.prototype; - 以
obj为this执行Foo.call(obj, args); - 如果构造函数返回的是对象则返回该对象,否则返回
obj。
正是 第 2 步 把实例挂到了原型链上。因此 Foo.prototype 上定义的方法,可以被所有实例共享:
function Box(value) {
this.value = value; // 实例自有
}
Box.prototype.getValue = function () {
return this.value; // 共享方法
};
const b1 = new Box(1);
const b2 = new Box(2);
b1.getValue(); // 1
b2.getValue(); // 2
b1.getValue === b2.getValue; // true(同一个函数引用,节省内存)六、class 语法下的原型链
ES6 的 class 只是原型继承的语法糖,底层依然是原型链:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks.`;
}
}
const d = new Dog("Rex");
// 原型链:
// d ──▶ Dog.prototype ──▶ Animal.prototype ──▶ Object.prototype ──▶ null
Object.getPrototypeOf(d) === Dog.prototype; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // trueextends 做的事情,等价于 Object.setPrototypeOf(Dog.prototype, Animal.prototype),并把静态方法也通过 Dog.__proto__ = Animal 链起来。
七、检查属性归属
由于原型链的存在,判断一个属性"是不是自己的"很重要:
function Graph() {
this.vertices = [];
}
Graph.prototype.addVertex = function (v) {
this.vertices.push(v);
};
const g = new Graph();
"vertices" in g; // true(包含原型链)
"addVertex" in g; // true(在原型上)
Object.hasOwn(g, "vertices"); // true ← 推荐
Object.hasOwn(g, "addVertex"); // false
g.hasOwnProperty("addVertex"); // false(老 API)for...in 会遍历原型链上的可枚举属性,而 Object.keys 只返回自有可枚举属性,这正是原型链带来的差异。
八、instanceof 的本质
a instanceof B 并不是看类型,而是 沿着 a 的原型链找有没有 B.prototype:
function instanceofPolyfill(obj, Ctor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Ctor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}理解了这一点,就能解释为什么修改原型链会影响 instanceof 的结果。
九、修改原型链的方式
| 方式 | 说明 | 备注 |
|---|---|---|
Object.create(proto) | 创建对象时指定原型 | ✅ 推荐 |
{ __proto__: proto, ... } | 字面量中指定 | ✅ 已标准化 |
Object.setPrototypeOf(obj, proto) | 运行时修改 | ⚠️ 性能极差 |
obj.__proto__ = proto | 旧写法 | ⚠️ 不建议 |
动态修改一个对象的
[[Prototype]]会让 V8 等引擎丢弃针对该对象的内联缓存与隐藏类优化,是 JS 中最慢的操作之一,应尽量在创建时就确定原型。
十、原型链的设计意义
- 共享与节省内存:方法挂在
prototype上,所有实例共用同一份函数引用; - 动态性:运行时修改原型即可让所有实例立刻获得新能力,这是 monkey patching、polyfill 的基础;
- 统一的对象模型:函数、数组、正则、甚至
class,最终都归并到同一套原型机制下,语言核心非常精简; - 比类继承更灵活:可以在任何时刻改变继承关系,构造出更动态的对象组合方式。
十一、一张图记住原型链
┌────────────┐
│ null │
└─────▲──────┘
│ [[Prototype]]
┌─────┴───────────┐
│ Object.prototype│◀────────────┐
└─────▲───────────┘ │
│ [[Prototype]] │
┌─────┴───────────┐ │
│ Foo.prototype │─constructor─▶ Foo (function)
└─────▲───────────┘ │
│ [[Prototype]] │ [[Prototype]]
┌─────┴───────┐ │
│ new Foo() │ ▼
└─────────────┘ Function.prototype- 实例的
__proto__→ 构造函数的prototype; - 构造函数的
prototype.constructor→ 构造函数自身; - 所有原型最终汇聚到
Object.prototype,再到null。
掌握了这张图,原型链的所有问题都只是它的一个切面。