菜单

JavaScript 执行模型

相关源文件

目的与范围

本文档解释了 JavaScript 引擎如何解析、编译和执行 JavaScript 代码。尽管许多开发人员认为 JavaScript 是一种纯粹的解释型语言,但它的执行模型实际上更为复杂,涉及解析、编译、优化和执行等多个阶段。理解这种模型对于编写高性能代码和有效调试至关重要。

有关 JavaScript 基础知识和语言特性的信息,请参阅 JavaScript 基础。有关 JavaScript 中通用编程概念的详细信息,请参阅 入门

JavaScript 执行模型概述

JavaScript 的执行模型介于传统的解释型语言和编译型语言之间。虽然 JavaScript 代码以源代码(而非二进制)的形式分发,但现代 JavaScript 引擎在执行前采用复杂的技术来解析和转换代码。

来源: get-started/ch1.md313-378 get-started/ch2.md18-38

JavaScript 的执行涉及几个不同的阶段

  1. 解析:源代码被解析成抽象语法树 (AST)
  2. 编译:AST 被转换为字节码
  3. 执行:字节码被解释和执行
  4. 优化:JIT 编译器识别并优化热代码路径

这个多步骤过程允许 JavaScript 引擎在执行开始前检测错误,并在运行时应用复杂的优化。

解析阶段

在执行任何 JavaScript 代码之前,必须先对其进行解析。解析器读取源代码文本并将其转换为称为抽象语法树 (AST) 的数据结构。

来源: get-started/ch1.md342-350

在解析过程中

  1. 代码被分解为标记(词法分析)
  2. 语法根据语言语法规则进行验证
  3. 早期错误(如语法错误)被检测到
  4. 创建程序的结构化表示(AST)

JavaScript 是一种被解析的语言,而不是纯粹的解释型语言,这可以从“早期错误”的存在中得到证明——这些错误在任何代码执行开始之前就被检测并报告,例如函数中重复的参数名。

“JS 源代码在执行前会被解析。规范要求如此,因为它需要‘早期错误’——代码中静态确定的错误,例如重复的参数名——在代码开始执行之前就被报告。如果不解析代码,这些错误就无法识别。” - get-started/ch1.md348-349

编译阶段

解析完成后,JavaScript 引擎将 AST 转换为一种优化的形式,通常是某种形式的字节码或中间表示 (IR)。

来源: get-started/ch1.md351-367

编译过程包括

  1. 将解析后的 AST 转换为字节码
  2. 应用初步优化
  3. 为解释器执行准备代码

这一步是 JavaScript 被更准确地描述为编译型语言而非纯解释型语言的原因:“解析后的 JS 被转换为优化的(二进制)形式,然后执行该‘代码’” get-started/ch1.md352-353

执行模型

编译成字节码后,JavaScript 代码由 JavaScript 虚拟机执行。现代 JavaScript 引擎结合使用解释和即时 (JIT) 编译来实现最佳性能。

来源: get-started/ch1.md356-359 get-started/ch1.md385-391

执行过程通常包括

  1. 解释:字节码最初由解释器执行
  2. 分析:引擎监控执行情况以识别频繁运行的代码(“热”代码路径)
  3. JIT 编译:频繁执行的代码被编译成优化的机器码
  4. 去优化:如果优化过程中做出的假设在运行时被证明是错误的,代码将被去优化回解释执行

这种混合方法允许 JavaScript 通过解释快速启动,同时通过 JIT 编译实现热代码路径的高性能。

程序生命周期

JavaScript 将每个文件视为一个独立的程序单元,这对代码的处理和执行方式有影响。

来源: get-started/ch2.md18-38

JavaScript 程序生命周期的关键特征

  1. 文件独立性:“在 JS 中,每个独立的 are 文件都是它自己的程序” get-started/ch2.md22
  2. 错误隔离:一个文件中的故障不一定妨碍其他文件的处理
  3. 全局共享:多个文件可以通过全局作用域进行交互——“多个独立的 .js 文件作为一个整体程序运行的唯一方式是通过‘全局作用域’共享它们的状态(及其公共功能的访问权)” get-started/ch2.md32-33
  4. 模块系统:ES6 模块提供了全局作用域共享的替代方案

转译和构建过程

现代 JavaScript 开发通常涉及一个构建过程,该过程在代码到达 JavaScript 引擎之前对其进行转换。

来源: get-started/ch1.md206-310

现代 JavaScript 构建管道通常包括

  1. 转译:将较新语法转换为等效的旧语法,以实现向后兼容
  2. Polyfills:为旧环境添加缺失的 API 实现
  3. 打包:将多个文件合并为更少的交付包
  4. 最小化:通过删除不必要的字符来减小代码大小

这个构建过程有助于弥合现代 JavaScript 功能与需要支持它们的环境之间的差距:“转译是一个人为的、社区发明的术语,用于描述使用工具将程序的源代码从一种形式转换为另一种形式(但仍是文本源代码)” get-started/ch1.md216-217

JavaScript 引擎内部

现代 JavaScript 引擎包含多个协同工作的组件,以高效地执行代码。

来源: get-started/ch1.md356-367 get-started/ch1.md385-391

JavaScript 引擎的关键组件包括

组件描述
解析器将源代码转换为 AST,检测语法错误
编译器将 AST 转换为字节码或机器码
解释器逐行执行字节码
JIT 编译器优化频繁执行的代码路径
分析器监控执行以识别优化机会
垃圾收集器通过移除未使用的对象来管理内存

V8 (Chrome/Node.js)、SpiderMonkey (Firefox) 和 JavaScriptCore (Safari) 等现代引擎都遵循类似的架构,同时实现不同的优化策略。

WebAssembly (WASM) 集成

WebAssembly (WASM) 代表了浏览器环境中代码执行方式的演进,它补充了 JavaScript 的执行模型。

来源: get-started/ch1.md384-411

WASM 在几个关键方面与 JavaScript 不同

  1. 预编译:WASM 是提前 (AOT) 编译的,而不是在运行时
  2. 二进制格式:WASM 以二进制格式分发,而不是源代码
  3. 最小化处理:“WASM 是一种更接近汇编的表示格式,JS 引擎可以通过跳过 JS 引擎通常进行的解析/编译来处理它” get-started/ch1.md397-398
  4. 性能重点:WASM 在设计时就考虑了性能

WASM 不会取代 JavaScript,而是对其进行补充,允许使用 C++ 或 Rust 等语言编写性能关键的代码,同时仍能在浏览器环境中与 JavaScript 进行互操作。

对性能的影响

了解 JavaScript 的执行模型有助于编写高性能代码。

来源: get-started/ch1.md385-391

基于执行模型的性能考虑

  1. 解析成本:文件越大,解析时间越长;考虑代码拆分
  2. 有利于优化的模式:一致的类型和可预测的代码流程有助于 JIT 优化
  3. 去优化触发器:某些模式可能导致优化代码回退到解释执行
  4. 内存管理:对象创建和垃圾回收模式会影响性能

许多优化策略都是顺应 JavaScript 引擎的执行方式,而不是与之对抗。

不同环境中的 JavaScript

JavaScript 的执行模型在不同环境中略有差异。

环境引擎示例独特特征
浏览器V8 (Chrome), SpiderMonkey (Firefox), JavaScriptCore (Safari)DOM 集成、安全沙箱
Node.jsV8文件系统访问、单线程事件循环与异步 I/O
DenoV8默认安全,内置 TypeScript 支持
移动应用JavaScriptCore (React Native), V8 (NativeScript)资源有限,可能存在 JIT 限制

来源: get-started/ch1.md86-105 get-started/ch1.md128-154

虽然核心执行模型相似,但每个环境都提供了不同的 API 和优化优先级。浏览器 JavaScript 引擎必须处理各种 Web 内容,而 Node.js 则针对服务器工作负载进行优化。

严格模式和执行

严格模式通过强制执行更严格的解析和错误处理来影响 JavaScript 代码的执行方式。

来源: get-started/ch1.md412-478

严格模式(在 ES5 中引入)通过以下方式影响执行模型:

  1. 将某些静默错误转换为抛出的错误
  2. 禁止某些有问题的或为未来版本保留的语法
  3. 禁用令人困惑或设计不佳的功能
  4. 通过禁止某些代码模式来启用优化

严格模式通过在文件或函数顶部放置 "use strict"; 来启用,并且“不应被视为对您不能做什么的限制,而应被视为最佳方式的指导” get-started/ch1.md417-418

总结

JavaScript 的执行模型将解释和编译的元素结合在一个复杂的管道中,该管道同时支持快速启动和优化的性能。执行过程包括解析、编译为字节码、解释以及热代码路径的即时优化。

理解这个模型可以帮助开发者更高效地编写代码,通过顺应 JavaScript 引擎的运行方式,而不是与之对抗。虽然 JavaScript 最初是一个简单的脚本语言,但现代的 JavaScript 引擎采用了先进的技术,使其性能堪比传统的编译型语言,同时又保留了动态类型和运行时求值的灵活性。

来源: get-started/ch1.md313-378 get-started/ch1.md385-411