彻底搞懂 JavaScript 原型链 (一篇就够了)

彻底搞懂 JavaScript 原型链 (一篇就够了)
Rainbow Bubbles彻底搞懂 JavaScript 原型链 (一篇就够了)
你是否曾经想过,为什么你创建一个简单的 JavaScript 对象 const obj = {},明明它“什么都没有”,但你却可以调用 obj.toString() 或者 obj.hasOwnProperty() 这样的方法?
这些方法是从哪里来的?
答案就是 原型链 (Prototype Chain)。
核心思想:它不是复制,而是“委托”
在很多面向对象语言(比如 Java 或 C#)中,“继承”通常意味着“复制”。子类会把父类的属性和方法复制一份作为自己的。
但在 JavaScript 中,继承的机制完全不同。它不是复制,而是委托 (Delegation)。
一个简单的比喻:向上级求助
- 你(实例 instance)接到了一个任务(比如 toString())。
- 你先看自己的任务列表(自身属性)里有没有这个任务。
- 有:你立刻自己搞定。
- 没有:你不会说“我不会”,而是把任务“委托”给你的直属上司(原型 prototype)。
- 你的上司(原型)也重复这个过程:
- 有:他来搞定。
- 没有:他再“委托”给他的上司(原型的原型)。
- 这个“委托”链条会一直持续下去,直到找到任务,或者到达公司的最高层 CEO(也就是 Object.prototype)。
- 如果 CEO 也不会(或者他没有上司了,即 null),那只能告诉你:这个任务做不了(返回 undefined 或报错)。
这个“你 -> 上司 -> CEO -> (null)”的查找链条,就是原型链。
两大核心:prototype 和 proto
要理解原型链,你必须先(并且要永远)区分这两个属性。它们是所有混乱的根源。
- prototype (显式原型)
- 谁有它? 函数 (Function)。
- 它是什么? 它是一个对象。
- 有什么用? 当这个函数被用作构造函数(通过 new 关键字)来创建新对象时,这个 prototype 对象就会被自动分配为新创建实例的“原型”。
- 目的? 它的存在,就是为了让所有由它创建的实例 共享 属性和方法。这极大地节省了内存。
1 | // 1. 定义一个构造函数 |
在这里,Foo.prototype 就像一个“共享技能包”。
2. proto (隐式原型)
- 谁有它? 几乎所有的 JavaScript 对象 (实例)。
- 它是什么? 它是一个指针(或引用)。
- 有什么用? 它指向创建这个对象的构造函数的 prototype。
- 目的? 这就是原型链“链接”的关键。当你访问一个对象的属性时,JavaScript 引擎就是通过这个 proto 指针去查找它的“上司”的。
注意:proto 是一个非标准的历史遗留属性。在现代 JavaScript 中,官方推荐使用 Object.getPrototypeOf(obj) 来访问,使用 Object.setPrototypeOf(obj, proto) 来设置。但为了教学和理解,proto 更直观。
魔法时刻:new 到底做了什么?
prototype 和 proto 是如何关联起来的?答案就在 new 关键字。
1 | const foo = new Foo('张三'); |
avaScript 引擎在背后(大致)做了这四件事:
- 创建新对象:创建一个全新的空对象 {}。
- 链接原型:将这个新对象的 proto 属性指向构造函数(Foo)的 prototype 对象。新对象.proto = Foo.prototype; (这是最关键的一步!)
- 绑定 this:将构造函数(Foo)的 this 指向这个新对象,并执行函数体(即 this.name = ‘张三’)。
- 返回新对象:返回这个被“加工”过的新对象(foo)。
现在,foo 这个实例就通过它的 proto 链接到了 Foo.prototype 这个“共享技能包”上。
完整链条:一个详细的查找过程
让我们把所有东西串起来,看看原型链是如何工作的。
1 | // 构造函数 |
- 场景一:foo.name
- JS 查找 foo 对象。
- foo 啊,你自己有 name 属性吗?
- foo:有!在构造函数里刚赋值的,是 ‘张三’。
- 查找结束。返回 ‘张三’。
- 场景二:foo.sayHello()
- JS 查找 foo 对象。
- foo 啊,你自己有 sayHello 方法吗?
- foo:没有。
- JS:好的,我去你的 proto 上找。
- JS 找到了 foo.proto,它发现这等于 Foo.prototype。
- Foo.prototype 啊,你有 sayHello 方法吗?
- Foo.prototype:有!在这里。
- 查找结束。执行这个方法。
- 场景三:foo.toString() (最关键)
- JS 查找 foo 对象。
- foo 啊,你有 toString 吗? -> 没有。
- JS 沿着 foo.proto 找到 Foo.prototype。
- Foo.prototype 啊,你有 toString 吗? -> 没有。
- JS:好的,我去你的 proto 上找。 (这一步是链条的延伸!)
- JS 找到了 Foo.prototype.proto。
- Foo.prototype 本身是一个普通对象,它是由 Object 构造函数创建的。因此,Foo.prototype.proto 指向的就是 Object.prototype。
- Object.prototype 啊,你有 toString 吗?
- Object.prototype:有!我这里有所有对象通用的 toString,hasOwnProperty 等方法。
- 查找结束。执行这个方法。
- 场景四:foo.someNonExistentMethod()
- 重复上述过程,一直找到 Object.prototype
- Object.prototype 啊,你有 someNonExistentMethod 吗? -> 没有。
- JS:好的,我去你的 proto 上找。
- JS 找到了 Object.prototype.proto,发现它是 null。
- 查找结束。null 代表链条的终点,不能再找了。
- 如果是在获取属性,就返回 undefined。如果是在调用方法,就抛出错误 TypeError: foo.someNonExistentMethod is not a function。
原型链关系图

关键总结:
- 实例 foo 通过 proto 链接到 Foo.prototype。
- Foo.prototype (它也是个对象) 通过 proto 链接到 Object.prototype。
- Object.prototype 通过 proto 链接到 null。
这就是 foo 对象的完整原型链。
还有两种改变原型链的方式
Object.create()
这是另一种创建对象的方式,它可以让你显式指定新对象的原型。
1 | const FooPrototype = { |
- ES6
class语法糖
ES6 引入了 class 关键字,这让 JavaScript 看起来更像传统的面向对象语言。但请记住:
class 只是原型继承的“语法糖”!它的底层实现… 完完全全… 还是原型链!
下面的 class 写法 等价于 我们上面写的 function Foo 的写法:
1 | class Foo { |
总结:为什么原型链如此重要?
- 实现继承:它是 JS 继承的根基。class 的 extends 关键字,本质上也是在操作原型链。
- 节省内存:所有实例共享原型上的方法。你创建一千个 Foo 实例,sayHello 方法在内存中也只存在一份(在 Foo.prototype 上),而不是一千份。
- 理解 JS:不理解原型链,你就无法真正理解 JS 中的对象、this 的指向、以及 class 的工作原理。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Rainbow Bubbles of Blog!
评论
匿名评论隐私政策



