My App

原型链

一、什么是原型链

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 引擎会执行:

  1. obj 自身属性中查找 x,找到则返回;
  2. 找不到则沿 obj.[[Prototype]] 向上查找;
  3. 重复第 2 步,直到找到 x 或某一层的 [[Prototype]]null
  4. 全程未找到则返回 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) 创建的对象 没有原型,也就没有 hasOwnPropertytoString 等方法,常用于实现纯净的"字典对象"。

const dict = Object.create(null);
dict.hasOwnProperty; // undefined

五、new 运算符与原型链的建立

new Foo(args) 的内部步骤大致如下:

  1. 创建一个新对象 obj = {}
  2. obj.[[Prototype]] 指向 Foo.prototype
  3. objthis 执行 Foo.call(obj, args)
  4. 如果构造函数返回的是对象则返回该对象,否则返回 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; // true

extends 做的事情,等价于 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 中最慢的操作之一,应尽量在创建时就确定原型。

十、原型链的设计意义

  1. 共享与节省内存:方法挂在 prototype 上,所有实例共用同一份函数引用;
  2. 动态性:运行时修改原型即可让所有实例立刻获得新能力,这是 monkey patching、polyfill 的基础;
  3. 统一的对象模型:函数、数组、正则、甚至 class,最终都归并到同一套原型机制下,语言核心非常精简;
  4. 比类继承更灵活:可以在任何时刻改变继承关系,构造出更动态的对象组合方式。

十一、一张图记住原型链

            ┌────────────┐
            │   null     │
            └─────▲──────┘
                  │ [[Prototype]]
            ┌─────┴───────────┐
            │ Object.prototype│◀────────────┐
            └─────▲───────────┘             │
                  │ [[Prototype]]           │
            ┌─────┴───────────┐             │
            │  Foo.prototype  │─constructor─▶ Foo (function)
            └─────▲───────────┘             │
                  │ [[Prototype]]           │ [[Prototype]]
            ┌─────┴───────┐                 │
            │  new Foo()  │                 ▼
            └─────────────┘          Function.prototype
  • 实例的 __proto__ → 构造函数的 prototype
  • 构造函数的 prototype.constructor → 构造函数自身;
  • 所有原型最终汇聚到 Object.prototype,再到 null

掌握了这张图,原型链的所有问题都只是它的一个切面。

On this page