技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

译 JavaScript 引擎基础:原型优化

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/09-[译] JavaScript 引擎基础:原型优化
本文目录7 个章节
  1. 0. 先区分规范与实现
  2. 一、优化层级与执行效率的取舍
  3. 二、Shape、Hidden Class 与属性访问
  4. 三、Inline Cache 与原型依赖
  5. 四、实际编码建议
  6. 五、总结
  7. 参考资料

JavaScript 引擎基础:原型优化

Category(分类): JavaScript Status: 已更新

本文保留 2018 年原译文关于 Shapes、Inline Caches 和原型属性访问的主要内容,并把当时的引擎实现与今天的通用结论分开。文中的 ShapeHiddenClassMapStructureValidityCell 都是引擎实现术语,不是 ECMAScript 可以直接调用的 API。

原文作者:Benedikt Meurer、Mathias Bynens 原译文作者:hijiangtao

原文:JavaScript engine fundamentals: optimizing prototypes 上一篇译文:JavaScript 引擎基础:Shapes 和 Inline Caches

0. 先区分规范与实现

ECMAScript 规定了对象属性、原型链和类的可观察行为,但没有规定:

  • 引擎必须使用解释器还是 JIT;
  • 必须有多少个编译层;
  • Shape 是否叫 HiddenClass、Map 或 Structure;
  • Inline Cache、ValidityCell 或去优化必须采用什么实现;
  • 某种引擎一定比另一种引擎快。

因此,本文的性能部分应理解为帮助建立实现模型的历史资料,不能把某个版本 V8 的内部结构当成跨浏览器、跨版本的固定事实。

一、优化层级与执行效率的取舍

现代 JavaScript 引擎通常在“尽快启动代码”和“让热点代码运行得更快”之间做取舍。解释器启动成本低、生成代码快;基线或优化编译器需要更多编译时间和内存,但可能生成运行效率更高的机器码。

原文:JavaScript 引擎的优化流程

原文:启动速度与运行速度之间的取舍

1. V8 的历史与现代分层

原文写作时主要介绍了 V8 的 Ignition 解释器和 TurboFan 优化编译器,并把它们描述为简单的两层模型:

原文:V8 的旧优化流程

V8 的现代管线已经更加分层,常见的概念模型是:

JavaScript 源码
    ↓
Ignition:解释执行字节码并收集反馈
    ↓
Sparkplug:快速基线编译
    ↓
Maglev:中层优化编译
    ↓
TurboFan:更深入的优化编译

并不是每个函数都会依次经过所有层级。引擎会根据代码是否变热、反馈信息、代码大小、内存和编译成本等因素作决定,还可能因为假设失效而去优化(deoptimize),回到较低层级或解释执行。

原文中“Ignition 是所有引擎中最快的解释器”属于没有版本、平台和基准条件的绝对排名,应删除。更准确的说法是:解释器通常有利于快速启动,优化编译器通常有利于热点代码的长期运行性能,但具体结果必须以目标引擎和真实工作负载测试为准。

原文:代码进入 V8 优化器

2. SpiderMonkey、Chakra 和 JavaScriptCore

原译文还介绍了 SpiderMonkey、Chakra 和 JavaScriptCore 的分层:

原文:SpiderMonkey 的历史优化流程

原文:SpiderMonkey 进入更高优化层

原文:Chakra 的历史并行编译流程

原文:JavaScriptCore 的历史并发编译流程

这些内容有重要的历史价值,但不能直接当成 2026 年所有版本的当前架构:

  • Chakra/ChakraCore 主要是旧 EdgeHTML 时代的引擎;现代 Microsoft Edge 基于 Chromium/V8;
  • SpiderMonkey 的实现持续演进,旧资料中的 IonMonkey 术语不能覆盖今天的完整管线;
  • JavaScriptCore 的 LLInt、Baseline JIT、DFG、FTL 等层级和调度策略也会随版本变化;
  • 不同宿主、平台和编译选项可能导致同一引擎采用不同策略。

引擎的共同目标仍然是:先尽快运行,再利用类型反馈和执行频率优化热点代码,同时控制编译延迟、内存使用和去优化成本。

原文:优化机器码的内存成本

3. 字节码和机器码的内存取舍

原文用一个简单的加法函数说明,优化后的机器码可能比解释器字节码占用更多内存:

function add(x, y) {
  return x + y
}

add(1, 2)

解释器可以使用紧凑的字节码,执行时由解释器逐条处理;优化编译器则可能生成更多机器指令,并附带保护检查、去优化入口和元数据。优化不是“越多越好”:

  • 冷代码不值得花大量时间编译;
  • 编译过程本身会消耗 CPU 和内存;
  • 过于激进的假设失效后可能反复去优化;
  • 代码缓存和机器码占用也会影响整体性能。

原文:解释器、编译器与内存的取舍

二、Shape、Hidden Class 与属性访问

1. 什么是 Shape

JavaScript 对象可以动态添加、删除属性,属性的值和属性描述符也可以变化。为了更快地表示常见对象,引擎通常会把:

  • 对象的结构信息:有哪些属性、属性布局、描述符、原型等;
  • 对象实际保存的属性值;

分开处理。不同引擎的术语不同:V8 常用 Hidden Class/Map,SpiderMonkey 常用 Shape,JavaScriptCore 常用 Structure。本文统一用 Shape 表示这类概念。

function Point(x, y) {
  this.x = x
  this.y = y
}

const first = new Point(1, 2)
const second = new Point(3, 4)

如果实例以相同顺序初始化相同属性,通常更容易共享结构信息:

原文:Shape 与对象值分离

但不要把“同样的属性”理解得过于绝对。属性创建顺序、删除属性、改变属性描述符、改变原型以及特定类型的内部表示,都可能影响引擎的布局或优化策略。修改一个已有数据属性的值,通常不等于改变 Shape;但它仍可能影响类型反馈或原型查找依赖。

2. class 与原型

原文用 class 和构造函数表示同一组方法:

class Bar {
  constructor(x) {
    this.x = x
  }

  getX() {
    return this.x
  }
}

const foo = new Bar(true)

简单地近似改写为:

function BarLegacy(x) {
  this.x = x
}

BarLegacy.prototype.getX = function getX() {
  return this.x
}

这个例子说明了类方法通常放在原型上、实例字段通常放在实例自身,但 class 不只是无条件的语法替换。类还具有严格模式、不能直接调用 class 构造器、方法默认不可枚举、extends/super、私有字段、类字段和静态初始化块等规范语义。上面的函数版本只用于说明原型共享机制。

原文:class 实例与原型

原文:类原型对象的属性

如果创建另一个 Bar 实例,两个实例通常可以共享相同的结构和 Bar.prototype

const another = new Bar(false)

Object.getPrototypeOf(foo) === Bar.prototype // true
Object.getPrototypeOf(another) === Bar.prototype // true

3. 属性读取与方法调用

下面的调用可以从理解层面拆成“先读取方法,再以 foo 作为接收者调用”:

const value = foo.getX()

// 近似理解为:
const getX = foo.getX
const valueAgain = getX.call(foo)

这不是说引擎一定真的生成这两行代码,而是为了说明 this 来自方法调用表达式的接收者。属性读取会先查对象自身,再沿原型链向上查找:

原文:从实例开始查找原型属性

4. 原型链是可变的

JavaScript 允许改变对象的原型,因此同一个属性访问点的结果可能发生变化:

const item = new Bar(true)

item.getX() // true

Object.setPrototypeOf(item, null)

// item.getX()
// TypeError: item.getX is not a function

Object.setPrototypeOf() 是标准 API,但对已经运行的热点对象修改原型可能让引擎放弃或重建相关优化;这不是语义错误,而是应谨慎使用的动态特性。创建对象时确定原型通常更容易让引擎优化。

原文:修改实例原型后的属性访问

5. DOM 原型链只是示例,不要依赖固定层数

原文使用锚元素的 getAttribute() 说明 DOM 原型链可能比自定义类更长:

const anchor = document.createElement('a')
const title = anchor.getAttribute('title')

具体方法位于哪一层、原型链有多少层,会因浏览器和版本而变化,不能把“6 个原型”“7 次检查”作为固定规则。对属性读取而言,抽象模型仍然是:

  1. 检查接收者自身是否有属性;
  2. 沿 [[Prototype]] 查找;
  3. 找到数据属性或访问器后按规范读取;
  4. 到达 null 仍未找到则返回 undefined

原文:DOM 对象的原型链

三、Inline Cache 与原型依赖

1. Inline Cache 的直觉

当同一个代码位置反复读取相似结构的对象时,引擎可以记录过去的反馈,并在下一次访问时先检查少量条件:

  • 这个接收者是否具有预期结构;
  • 原型链是否仍满足查找假设;
  • 目标属性的布局或值是否仍可直接使用。

命中时可以减少重复的通用查找;不命中时回退到更通用的路径,并更新反馈。常见术语包括:

  • 单态(monomorphic):一个访问点主要看到一种结构;
  • 多态(polymorphic):看到少量结构;
  • 超多态(megamorphic):结构种类很多,专门化收益降低。

这些都是实现层面的概念,不是开发者可以强制设置的开关。

原文:把原型链信息折叠到 Shape

2. 原型保护与 ValidityCell

原译文以 2018 年 V8 为例,介绍 ValidityCell:当相关原型或祖先原型改变时,某个依赖可能失效,Inline Cache 下次命中时就不能继续使用旧假设。

原文:原型 Shape 与 ValidityCell

原文:Inline Cache 保存的字段

这个模型可以帮助理解“为什么原型改变可能造成去优化”,但 ValidityCell 不是 ECMAScript 概念,也不应假设今天所有 V8 版本都以完全相同的内部结构实现。

在概念上,第一次访问可能记录:

  1. 接收者结构;
  2. 找到属性的原型;
  3. 属性在该结构中的布局信息;
  4. 能证明原型链仍未改变的依赖。

之后的访问只需验证这些假设:

原文:Inline Cache 命中后的快速访问

如果假设失效,引擎可以使相关缓存失效、回退到通用查找或重新编译。并不是所有缓存都会失效,实际影响取决于哪些优化代码依赖这条原型链。

原文:原型变化导致缓存失效

3. 修改原型为什么通常不推荐

下面的代码修改了所有普通对象常见的祖先原型:

Object.prototype.newMethod = function newMethod() {
  return 'example'
}

// 依赖 Object.prototype 的属性查找可能需要重新检查或去优化。
const result = ({ }).newMethod()

delete Object.prototype.newMethod

不推荐运行时修改内建原型,原因不只是性能:

  • 可能让依赖该原型链的优化代码失效;
  • 可能与其他库、框架或未来标准属性发生命名冲突;
  • 可能改变第三方代码对属性存在性的判断;
  • 可能带来安全和调试问题。

如果需要兼容旧环境,优先使用特性检测、命名空间函数或模块内工具;确实需要 polyfill 时,应在应用代码运行前按标准定义,并遵循兼容性条件。

四、实际编码建议

1. 在构造函数中稳定初始化字段

class User {
  constructor(id, name) {
    this.id = id
    this.name = name
    this.role = undefined
  }
}

统一初始化顺序有助于产生相似对象结构,但不要为了追求某个 Shape 而牺牲可读性。现代引擎会持续优化许多自然写法,最终应以真实性能数据为准。

2. 不要在热点路径频繁改变原型

// 更推荐:创建时确定原型
const user = Object.create(userPrototype)

// 谨慎:对象已经被大量使用后再修改原型
Object.setPrototypeOf(user, anotherPrototype)

如果需要不同类型的对象,优先使用明确的类、工厂或 Object.create() 在创建阶段设置原型。

3. 不要把引擎优化建议当成语言规则

以下说法都不应作为绝对结论:

  • “所有对象都必须按同一顺序创建属性”;
  • “修改一个属性值一定会改变 Shape”;
  • “修改一个原型会让所有 Inline Cache 失效”;
  • “某个引擎一定比其他引擎快”;
  • “只要使用 class 就一定比对象字面量快”。

这些说法脱离引擎版本、运行平台、数据规模和实际工作负载后都可能失真。

五、总结

  • JavaScript 引擎常用分层执行、类型反馈和 Inline Cache 来平衡启动时间与热点性能;
  • Shape/Hidden Class 等结构信息帮助引擎描述对象布局,但名称和策略属于实现细节;
  • 原型属性读取需要同时考虑对象自身、原型链、访问器、Proxy 和动态修改;
  • class 方法通常利用原型共享,但 class 还有严格模式、私有元素等额外语义;
  • 原型或内建对象的运行时修改可能使相关优化失效,并带来兼容性和安全风险;
  • 不要凭历史文章或微基准过度手动“迎合引擎”,先保持清晰代码,再用目标环境的真实数据验证。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS