原型链我最早只记”向上查找”,后来才把instanceof的原理和修改Object.prototype的性能代价串起来。
Q1: 原型链的查找机制
1
1995年: JS 的原型继承设计
JavaScript 没有采用传统的类继承,而是采用了原型继承——每个对象都有一个内部链接指向另一个对象(它的原型),形成一条链。
2
致命痛点: 方法复用
如果每个实例都独立拥有一份方法的拷贝,内存消耗会爆炸。原型链让所有实例共享原型上的方法,只存一份。
3
核心机制: 沿 proto 向上攀升
当访问一个对象的属性时,如果对象自身没有,引擎就会沿着它的
__proto__ 链条一直往上找,直到找到目标属性或者到达 null 为止。4
V8 的极致优化: Inline Cache
V8 并没有把对象当成哈希表,而是为每个对象生成了 Hidden Class(Map)。第一次访问
obj.foo 沿原型链找到后,V8 会缓存结果(Inline Cache)。下次同一行代码再访问时,直接根据缓存的偏移量取值,O(1) 复杂度。5
关键洞察: 动态修改原型是致命的
给
Object.prototype 加属性会导致所有对象的 Map 失效,Inline Cache 随之失效,性能明显下降。原型链是 JS 实现动态继承的基石,V8 用 Inline Cache 让热点访问在命中缓存后降到 O(1)。
错误示范: 原型链污染
直接修改全局构造函数的 prototype,会顺着原型链波及所有数组实例,引发极其难以排查的 Bug。
正确解法: 安全的字典对象
如果你只需要一个纯粹的 Key-Value 映射字典,不需要继承任何对象方法。
Q2: 高频手写题:剥开原型链的伪装
instanceof 的底层原理
instanceof 的本质是沿着对象的 [[Prototype]] 链逐层向上查找,判断 right.prototype 是否在链上。它不能判断基本类型,也不能跨 iframe 使用(不同 iframe 的构造函数不同)。
手写 Object.create
Object.create(proto) 的本质是创建一个新对象并将其 [[Prototype]] 指向 proto:
底层源码:V8 的 Inline Cache 优化
原型链查找明明要一层层往上找,为什么在 V8 里读取对象属性依然极快?
V8 对原型链查找做了极致的优化(详见 V8 Blog: Inline Caches):
- Hidden Class(隐藏类 / Map):V8 并没有把对象当成哈希表,而是为每个对象在底层动态生成了一个类似 C++ 结构体的
Map(描述对象的内存布局和属性偏移量)。 - Inline Cache (内联缓存 IC):当 V8 第一次执行
obj.foo并沿着原型链千辛万苦找到目标时,它会立刻把这个结果连同当前对象的 Map 缓存起来。 - 跳过原型链:下次同一行代码再访问
obj.foo时,V8 会先比对当前对象的 Map 是否和缓存一致(Monomorphic 单态)。如果一致,直接根据缓存的内存偏移量去取值,时间复杂度 O(1),彻底跳过原型链查找!
十、及格回答 vs 满分回答
问题:“请谈谈你对 JavaScript 原型链的理解?”
及格回答(60-70分):“每个对象都有一个问题出在:标准课本背诵,缺乏对底层引擎设计的认知。 满分回答(90-100分):__proto__属性,指向它构造函数的prototype。当访问一个属性时,如果自己身上没有,就会顺着[[Prototype]]链一直往上找。我们在原型上写方法可以实现内存复用。”
“原型链是 JS 实现动态继承的基石。语言规范层面上,它的核心是沿着对象的[[Prototype]]指针不断向父级甚至null进行回溯属性查找的线性机制。[1]”
“为了突破逐层扫描的性能瓶颈,V8 引入了 Hidden Class(隐藏类/Map)与 Inline Cache(内联缓存)。当热点属性被命中后,V8 会直接将其内存偏移量缓存。下次访问时,只要对象形态不变,即可实现 O(1) 级的快速访问,完全跳过真正的链式遍历。[2]”
“基于上述底层优化,在实际业务中坚决反对随意篡改原生内置对象(如修改 Array.prototype)。这种操作会瞬间导致所有相关对象的 Map 失效,摧毁 V8 好不容易建立的 IC 缓存路径,引发大面积的性能断崖。[3]“
常见追问
Object.create(null) 和 {} 有什么本质区别?
Object.create(null) 和 {} 有什么本质区别?
答题要点:
原型链终点与纯净字典。高分回答:
“
{} 是通过 Object 构造函数生成的,它的 [[Prototype]] 依然指向 Object.prototype,因此它携带着 toString、hasOwnProperty 等一堆内置方法。而 Object.create(null) 会硬生生切断原型链,返回一个 [[Prototype]] 严格为 null 的纯净对象。它不仅没有原型链污染的风险,在 V8 眼里也是一个结构最简单的纯净字典,非常适合用来做大量数据的 Key-Value 映射。”原型链和作用域链有什么区别?
原型链和作用域链有什么区别?
答题要点:
从查找目标和查找方向两个维度对比。高分回答:
“原型链查找的是对象的属性,沿着
__proto__ 向上攀升,终点是 null。作用域链查找的是变量,沿着词法环境的 outer 指针向上攀升,终点是全局环境。两者是完全独立的机制:原型链是对象层面的继承关系,作用域链是代码层面的变量可见性。一个函数内部可以同时使用两条链——先沿作用域链找到变量(闭包),再沿原型链找到变量的方法。”如何判断一个属性是对象自身的还是继承自原型的?
如何判断一个属性是对象自身的还是继承自原型的?
答题要点:
hasOwnProperty vs in 操作符的区别。高分回答:
“用 注意:
hasOwnProperty 判断属性是否属于对象自身,用 in 操作符判断属性是否在对象上(包括原型链)。for...in 会遍历原型链上所有可枚举属性,所以通常需要配合 hasOwnProperty 过滤。ES2022 推荐使用 Object.hasOwn(obj, prop) 替代。”proto 和 prototype 的区别?
proto 和 prototype 的区别?
答题要点:
从所属对象和用途两个维度区分。高分回答:
“推荐用
__proto__ 是每个对象都有的内部属性([[Prototype]]),指向其构造函数的 prototype。prototype 只有函数才有,是函数的一个普通属性,用作 new 创建实例时的原型模板。Object.getPrototypeOf(obj) 替代 obj.__proto__。”如何实现 ES5 的类继承?
如何实现 ES5 的类继承?
答题要点:
寄生组合式继承是最佳方案。高分回答:
“ES5 最佳继承方案是寄生组合式继承:为什么不用
Child.prototype = new Parent()?因为构造函数中的副作用(如 this.name = name)会被执行两次,而且会把 name 等实例属性错误地放到原型上。”为什么修改 Object.prototype 会导致性能暴跌?
为什么修改 Object.prototype 会导致性能暴跌?
答题要点:
从 V8 Hidden Class 和 Inline Cache 的角度解释。高分回答:
“V8 为每个对象生成 Hidden Class(Map),描述对象的内存布局和属性偏移量。Inline Cache 缓存了 Map 与属性偏移量的映射关系。修改
Object.prototype 会导致所有对象的 Map 失效(因为所有对象的原型链都经过 Object.prototype),IC 缓存瞬间失效,V8 不得不重新逐层查找原型链,性能明显下降。手写一个 Object.create 并解释其原理
手写一个 Object.create 并解释其原理
答题要点:
本质是创建空对象并设置原型指向。高分回答:
“第二个参数
Object.create(proto) 的本质是创建一个新对象并将其 [[Prototype]] 指向 proto:propertiesObject 可以像 Object.defineProperties 一样定义属性描述符。Object.create(null) 创建的纯净对象没有原型链,适合做安全的字典。”prototype 的 constructor 属性有什么用?丢失了怎么办?
prototype 的 constructor 属性有什么用?丢失了怎么办?
答题要点:
constructor 指回构造函数,手动赋值原型对象时会丢失。高分回答:
“每个函数的 修复方式:手动补上
prototype 对象自带一个 constructor 属性指回函数自身。但当我们手动赋值 prototype 时会丢失:constructor。constructor 在 instanceof 中不参与判断,但对某些序列化和反射场景有用。“延伸阅读与引用参考
- V8 Deep Dive: Inline Caches:V8 官方揭秘内联缓存与原型链性能优化底层原理。 [1]
- MDN Web Docs: 继承与原型链:MDN 最权威的基础语言特性梳理。 [2]
- V8 Source Code: ic.cc:V8 内联缓存的核心实现源码。 [3]
