Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

Javascriptv8 引擎

在开始讲解 v8 之前,我们先来了解一下 Web 应用的生命周期。

典型客户端 Web 应用的生命周期从用户在浏览器地址栏输入一串 URL,或单击一个链接开始。当我们输入了一个 url https://www.bilibili.com/,其过程如下图所示:

这张图展示了一个 Web 页面生命周期的流程图,具体描述了从用户输入 URL 到页面关闭之间发生的一系列操作。那么我们应该是要知道当浏览器遇到了 JavaScript 文件之后到的是怎么处理的,也就是图中的第四个步骤。

什么是 V8

V8 是由 Google 开发并开源的高性能 JavaScript 和 WebAssembly 引擎,专注于性能优化,通过即时编译(JIT)技术将 JavaScript 代码直接编译为机器代码,而不是先解释再执行,从而大大提高了执行速度。V8 不仅能提供跨平台的统一执行环境,确保在 Windows、Linux、macOS 上高效和一致的执行表现,还能够独立运行或嵌入到任何 C++ 应用程序中。

V8 的主要特点和功能包括:

  1. 即时编译(JIT): V8 使用即时编译技术,将 JavaScript 代码编译成 CPU 可执行的二进制指令,而不是逐行解释执行。通过这项技术,V8 显著提高了 JavaScript 的执行效率,使其在浏览器和服务器端(如 Node.js)都能提供极高的性能。

  2. 垃圾回收(Garbage Collection): V8 内置了先进的垃圾回收机制,通过动态管理内存来避免内存泄漏。它采用了分代收集(Generational GC)和增量垃圾回收等技术,以减少暂停时间,提高程序的响应速度。

  3. 优化执行: V8 引擎不仅仅是将 JavaScript 编译成机器码,它还会在执行过程中对代码进行动态优化。例如,V8 会识别和优化”热路径”(即频繁执行的代码段),从而提高这些部分的执行效率。它使用 即时编译(JIT)优化 和 长期优化(Turbofan) 技术,使得代码执行更加高效。

  4. 支持 WebAssembly: 除了 JavaScript,V8 还支持 WebAssembly(Wasm)。WebAssembly 使得开发者能够以 C/C++ 或其他低级语言编写高性能的代码,并在浏览器中运行。V8 提供了对 Wasm 的高效支持,使得在 Web 环境中执行非 JavaScript 代码变得更加流畅和高效。

  5. 跨平台能力: V8 的设计确保它能够在多个平台上工作,抽象化了底层操作系统的差异,使得开发者不需要关注不同平台的具体细节。无论是桌面操作系统(如 Windows 和 macOS)还是服务器端(如 Linux),V8 都能提供一致的执行环境和性能。

  6. 嵌入式应用支持: V8 不仅仅局限于浏览器和 Node.js,它也可以被嵌入到任何 C++ 应用程序中,提供脚本化支持和扩展功能。许多应用程序,如游戏引擎、桌面软件和服务器应用,都可以通过嵌入 V8 引擎来提供 JavaScript 解释执行的功能。

V8 是怎么执行 JavaScript 代码的

首先我们先来看下面的流程图,以及官方提供的流程图:

根据上面的流程图,我们可以得知 V8 引擎工作流程优化解析:

  1. Blink 到 V8:当浏览器的 Blink 渲染引擎接收到 JavaScript 代码时,它首先会将代码传递给 V8 引擎。V8 会以不同编码格式(如 ASCII、LATIN1、UTF-8)接收这些代码,并将它们交给 Stream 处理。

  2. Stream 处理:Stream 将这些代码转换为 UTF-16 编码的代码单元,并传递给 Scanner。这时,V8 需要处理的是 Unicode 字符,而不是原始的 UTF-16 代码单元,Scanner 执行 词法分析,将代码转换成 tokens(词法单元)。

  3. 预解析和解析:

    • 预解析(PreParser):此步骤对 JavaScript 中尚未执行的函数进行提前解析,只会标记函数存在,并不会解析其内部实现。这样做是为了避免无谓的解析,提升执行效率。

    • 解析(Parser):当函数实际被调用时,V8 才会对其进行完整的解析,将代码转换为 抽象语法树(AST)。这时,V8 确定代码的作用域和上下文。

  4. 字节码生成与执行:V8 的解释器 Ignition 会将 AST 转换为 字节码(Bytecode),并开始执行。如果某段代码被频繁执行,V8 会将这段代码标记为热点代码。

  5. 优化编译与反优化:

    • 优化编译器 TurboFan:当 V8 发现某段代码是热点代码时,它会将该字节码传递给优化编译器 TurboFan,进一步优化为高效的机器码,以提升执行效率。

    • 反优化:由于 JavaScript 的动态特性,代码在执行过程中可能会修改对象结构或属性,导致先前优化过的机器码变得无效。此时,V8 会执行 反优化,使代码回退至解释执行,确保能够适应动态变化的结构。

  6. 最终执行:经过优化后的机器码在下次执行时可以大幅提高性能,但如果发生了结构变化,反优化会使得代码回退至解释执行。

V8 引擎通过灵活的预解析、解析、字节码生成和优化编译过程,在保证动态语言特性的同时,尽可能优化执行效率。在某些代码的重复执行时,V8 会通过动态优化和反优化机制,确保 JavaScript 的高效执行,同时适应对象结构的动态变化。

解释器 Ignition

Ignition 是 V8 引擎的解释器部分,它的主要职责是将 JavaScript 的 抽象语法树(AST) 转换为 字节码(Bytecode) 并执行这些字节码。

JavaScript 是一种 动态语言,并且 不确定性 高,这意味着代码的执行路径、对象结构、函数调用等特性,通常是无法预测的。直接将 JavaScript 编译成机器码会非常复杂,并且会浪费时间在不常用的代码上。

而使用 Ignition 这样的解释器,可以:

  1. 快速执行程序,避免在程序启动时就进行复杂的优化。

  2. 在执行过程中,动态监测代码并根据需要将“热代码”交给优化编译器(TurboFan),逐步提高执行效率。

Ignition 的工作流程如下所示:

  1. 输入:在 V8 中,JavaScript 代码首先通过词法分析器和解析器,转换为 抽象语法树(AST),它是代码的语法结构树。

  2. 输出:Ignition 的核心任务是将 AST 转换成 字节码,字节码是一种中间表示形式,比直接执行源代码要高效,但比机器码要抽象。字节码并不依赖于特定的硬件或操作系统,它是为了高效执行而设计的。

  3. 解释执行:字节码是 Ignition 解释器的输入,Ignition 逐条解释执行字节码,而不是直接执行 JavaScript 源代码。这个过程比直接解释 JavaScript 源代码要快,因为字节码是一种更紧凑的形式。

在 JavaScript 环境中,Ignition 通过生成更小的字节码来减少内存占用,特别适用于内存受限的设备。同时,Ignition 的字节码执行较为简单,不需要复杂的编译优化,能够快速在不同硬件平台上部署。

JIT(Just-In-Time)编译

JIT(Just-In-Time 编译)是一种 动态编译 技术,它在程序运行时将代码(通常是字节码)编译成机器码,并且会根据实际的运行情况进行优化。JIT 编译的核心目的是 提高性能,通过将热点代码编译为机器码,避免了解释执行带来的性能瓶颈。相比于传统的 提前编译(如 C/C++ 的编译过程),JIT 编译是在程序运行时动态进行的,它可以更灵活地优化代码,适应不同的执行环境。

JIT 编译的执行流程通常包括以下步骤:

  1. 源代码/字节码到中间代码:JavaScript 或其他动态语言的源代码首先被编译成字节码或中间代码,这些代码仍然不能直接在硬件上执行,而是需要通过解释器或者 JIT 编译器处理。

  2. 热点代码识别:JIT 编译器会在代码执行过程中监控哪些部分的代码频繁被执行。称为 “热点代码”。这些代码的性能对程序整体的执行效率有较大的影响,因此 JIT 编译器会优先考虑将这些热点代码编译为机器码。

  3. 动态编译:一旦 JIT 编译器发现某段代码频繁执行,它就会将这些热点代码编译为原生机器码。编译过程会使用一些优化策略,如内联函数、常量折叠、循环展开等,以减少不必要的计算,提高执行效率。

  4. 优化与缓存:JIT 编译器在编译过程中可以对代码进行动态优化,并缓存已编译的机器码。这样,下一次执行时可以直接使用机器码,而无需再次编译,从而进一步加速执行。

V8 引擎结合了 Ignition 和 JIT 编译,通过这种方式,它能够在 内存受限的设备 上提供更好的性能和内存管理,同时又能在需要性能优化时,利用 JIT 编译的强大优势。具体来说,V8 中的执行流程大致如下:

  1. 初步解释: 当 V8 引擎第一次加载 JavaScript 代码时,它会通过 Ignition 将代码编译成字节码并执行。这时,Ignition 负责以较低的内存开销执行代码,特别是在设备资源有限的情况下,能够避免过高的内存消耗。

  2. 热点代码识别: 在代码执行过程中,V8 引擎会监控哪些函数或代码段被频繁调用,这些代码称为 “热点代码”。对于这些热点代码,Ignition 会标记它们,并将它们交给 TurboFan(V8 的高级优化 JIT 编译器)进行进一步处理。

  3. JIT 编译优化: 一旦 V8 检测到某段代码是热点代码,它就会启动 JIT 编译器(如 TurboFan)。TurboFan 会将字节码编译为 高效的机器码,并根据运行时数据对代码进行进一步优化,例如内联、常量折叠、循环优化等。

  4. 缓存和使用: 编译后的机器码会被缓存,下一次执行相同的代码时,V8 就可以直接使用已经编译好的机器码,而不需要再次编译。这样可以显著提高执行速度。

  5. 内存管理: 通过 Ignition 和 JIT 编译的结合,V8 在确保较低内存开销的同时,还能动态提升执行性能。这种方式适合在内存受限的设备上平衡性能和资源消耗。

接下来我们看一下 Ignition 和 JIT 编译对比:

特性IgnitionJIT 编译
功能字节码解释器,负责将 JavaScript 转换为字节码并执行动态将字节码转换为原生机器码并优化
内存开销低内存消耗,生成小巧的字节码较高内存消耗,机器码占用更多内存
执行速度执行速度较慢,依赖逐行解释执行速度快,直接执行机器码
优化没有太多优化,仅进行简单执行进行深度优化,如内联、常量折叠、常量折叠等
适用场景适用于内存受限、对性能要求不那么高的设备适用于需要高性能的设备和代码段

Turbofan 优化编译器

Turbofan 是 V8 引擎中的 高级 JIT 编译器,它用于将 JavaScript 字节码编译成机器码,以优化性能。Turbofan 的设计目的是通过对代码进行深度优化,在保证跨平台兼容的同时,尽可能提高 JavaScript 执行的速度。与 V8 引擎中的其他编译器(如 Ignition)相比,Turbofan 更加注重执行效率和高级优化。

Turbofan 的工作原理如下:

  1. 类型推断和内联:Turbofan 会推断变量的类型,并对频繁使用的函数进行内联优化,避免函数调用的开销,直接插入代码中。

  2. 冗余消除:去掉重复的计算或不必要的操作,避免浪费计算资源。

  3. 逃逸分析:分析对象是否会离开当前作用域,如果不会,就可以将它分配到更高效的栈上,而不是堆上。

  4. 机器级优化:Turbofan 会根据不同的硬件架构生成高效的机器码,确保在各个平台上都能高效运行。

  5. 动态优化:它根据程序运行时的反馈,调整优化策略,使得 JavaScript 执行更加高效。

Turbofan 会生成 特定平台的机器码,这意味着它针对不同的硬件架构(如 Intel、ARM、PowerPC、MIPS 等)生成相应的 原生机器码。这些机器码是与目标平台的 CPU 指令集兼容的低级代码,能够直接由 CPU 执行。

具体编译流程如下图所示:

什么是抽象语法树

抽象语法树(AST,Abstract Syntax Tree) 是一种以树形结构表示源代码语法的中间表示方式。它展示了程序的结构,而不仅仅是程序的文本顺序。AST 通过将代码中的每个元素转换为树的节点来帮助编译器或解释器分析和理解代码的语法和逻辑。

抽象语法树的构成主要有这两个方面组成:

  1. 节点:每个节点代表源代码中的一个构造元素(如表达式、运算符、语句、变量等)。这些节点通常不包含语法上的详细信息(比如括号、分号等),而是更关注代码的结构和逻辑。

  2. 边:边表示节点之间的关系或操作。它们连接父节点和子节点,帮助定义代码的控制流或数据流。

抽象语法树的生成过程

抽象语法树的生成过程也是由两部分组成:

  1. 词法分析(Lexical Analysis)

  2. 语法分析(Parsing)

词法分析(Lexical Analysis)

词法分析是将源代码转换为 词法单元(tokens) 的过程。源代码中的字符会被分解成一系列的词法单元,每个词法单元代表一个语言的基本构造,例如关键字、标识符、操作符、常量、符号等。

例如,对于以下 JavaScript 代码:

a + b * c;

词法分析的结果可能是:

  • a:标识符

  • +:加法运算符

  • b:标识符

  • *:乘法运算符

  • c:标识符

语法分析(Parsing)

语法分析是根据语言的语法规则,将词法单元组合成一个 抽象语法树(AST) 的过程。语法分析器会根据文法规则(例如 BNF 或 EBNF)来识别词法单元之间的关系,并将它们构建成树形结构。

在语法分析过程中,编译器会检查代码的结构是否符合该语言的语法规则。如果代码违反了语法规则(如缺少括号、分号错误等),语法分析器会报错。

对于以下表达式:

a + b * c;

在上面的代码中,a+b*c 都是词法单元(tokens)。语法分析会根据优先级规则解析 * 优先于 +,因此会首先处理 b * c,然后将其与 a 进行加法操作。

举个例子:

const moment = '777';

对源码的字符流进行扫描后根据构词规则识别单词,生成 token 流:

[ { "type": "Keyword", "value": "const" }, { "type": "Identifier", "value": "moment" }, { "type": "Punctuator", "value": "=" }, { "type": "String", "value": "'777'" } ]

在词法分析的基础上将单词序列组合成抽象语法树,如下图所示:

执行上下文

执行上下文(Execution Context)是 JavaScript 中的一个核心概念,用于描述代码在运行时所依赖的环境和状态。每当代码执行时,JavaScript 引擎都会创建一个执行上下文,来管理变量、函数、this 等的状态。执行上下文的核心概念包括三个主要部分,它们共同确保代码的正确执行。

执行上下文的核心概念

执行上下文的核心概念包括三个主要部分,它们共同确保代码的正确执行。

  1. 代码评估(Code Evaluation):执行上下文包含一个标志,表示当前代码的评估状态。它可以是 执行中、暂停 或 恢复 等状态。该状态用于确定代码是否正在执行、暂停或已经完成。

  2. 函数(Function):如果当前执行上下文正在评估一个函数对象的代码,那么该组件会保存这个函数对象。如果是脚本(script)标签或模块(module)代码,则该值为 null。

  3. 域(Realm):Realm 代表了 JavaScript 环境中的一个 “领域”。它包含了一组固有的对象、全局环境以及在该环境中加载的所有 ECMAScript 代码和其他资源。在不同的执行上下文中,Realm 可以代表不同的环境,例如浏览器的全局对象(window)或 Node.js 的全局对象(global)。

如下代码所示:

function moment() { const age = 18; const hobby = ['music']; function studyReact() { console.log('react真好玩'); } studyReact(); console.log(age, hobby); }

在上面的代码中,它的主要执行流程主要有以下几个方面:

  1. 执行 moment 函数时,JavaScript 引擎为 moment() 函数创建一个执行上下文,并将其推入执行栈。此时,moment 成为当前的执行上下文。

  2. 进入 moment 函数内部,首先声明了 age 和 hobby 变量,然后遇到函数调用 studyReact()。

  3. 当 studyReact 被调用时,JavaScript 引擎为 studyReact 创建一个新的执行上下文,并将该执行上下文推入栈顶。此时,studyReact 成为当前的执行上下文。

  4. 执行 studyReact 函数内部的代码,控制台打印出 “react 真好玩”。

  5. studyReact 执行完毕,当前的执行上下文会从执行栈中弹出,控制权返回到 moment 函数。

  6. 恢复执行 moment 函数,继续执行 console.log(age, hobby),输出 18 和 [“music”]。

  7. 完成,moment 函数的执行上下文从栈中弹出,整个代码执行过程结束。

执行上下文的类型

执行上下文的类型可以分为两个:

  1. 全局执行上下文: 这是默认的或基本的执行上下文。任何不在函数内部的代码位于全局执行上下文中。他在编译阶段便创建一个 Global 对象,在浏览器中它是 window 对象,由 var 创建的变量和全局声明的函数都会被挂载到 Global 对象上,此时 this 的值设置等于这个 Global 对象,一个程序中有且仅有一个全局执行上下文。

  2. 函数执行上下文: 每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。被调用的函数的执行上下文会被推送到栈顶并执行当前的执行上下文。

执行栈

执行栈 是一个栈结构,用于存储 执行上下文(Execution Context)。执行上下文代表了当前正在执行的代码环境,包含了代码运行所需的所有信息(如变量、函数、this 等)。

  • 栈 是一种数据结构,遵循 后进先出(LIFO,Last In, First Out) 的原则,意味着最后推入栈中的元素会最先被弹出。

  • 每当 JavaScript 需要执行代码时,它会为该代码创建一个新的执行上下文,并将其推入栈中。

  • 当前正在执行的上下文总是位于栈顶,栈底通常是全局执行上下文。

执行栈的工作原理主要有以下几个方面:

  1. 初始化阶段: 当 JavaScript 引擎开始执行程序时,它首先会创建一个全局执行上下文(Global Execution Context),并将其推入执行栈中。全局上下文是程序开始时存在的第一个上下文,包含了全局代码的执行环境。

  2. 函数调用: 每当遇到函数调用时,JavaScript 引擎会为该函数创建一个新的执行上下文,并将其推入执行栈顶。这个新的执行上下文会在栈顶成为当前正在执行的上下文。

  3. 函数执行完毕: 当函数执行完毕时,其执行上下文会从执行栈中弹出,控制权会返回到栈中的下一个执行上下文。

  4. 程序结束: 程序执行完成时,执行栈中的所有上下文都会被弹出。最终,栈底的全局上下文会被弹出,程序结束。

如下代码所示:

function first() { console.log('First'); second(); console.log('First again'); } function second() { console.log('Second'); } first();

上面的代码执行步骤如下:

  1. 全局执行上下文:程序开始时,JavaScript 引擎为全局代码创建一个全局执行上下文,并将其推入执行栈中。全局上下文是栈底的第一个元素,包含全局变量、函数声明等。

  2. 执行 first() 函数:调用 first() 函数时,JavaScript 引擎为其创建一个新的执行上下文,并将其推入栈顶,成为当前执行上下文。first() 函数开始执行,打印 "First"

  3. 调用 second() 函数:在 first() 函数内部,调用了 second() 函数。JavaScript 引擎为 second() 创建新的执行上下文,并推入栈顶,成为当前执行上下文。second() 执行并打印 "Second"

  4. second() 执行完毕:second() 函数执行完毕,其执行上下文从栈中弹出,控制权返回到 first() 函数。first() 函数继续执行,打印 "First again"

  5. first() 执行完毕:first() 函数执行完毕,其执行上下文从栈中弹出,控制权返回到全局上下文。

  6. 程序结束:所有函数执行完毕,执行栈变为空,程序结束。

尽管 JavaScript 是单线程的,但它也能够处理异步操作(如 setTimeout、AJAX 请求、事件监听等)。这通过 消息队列 和 事件循环(Event Loop) 来实现。在这些异步操作中,回调函数会被放入消息队列中,待执行栈为空时,才会将回调函数推入栈中执行。

如何创建执行上下文

执行上下文的创建分为两个阶段,第一阶段是创建阶段,第二个阶段是执行阶段。

创建阶段

在创建阶段,V8 引擎只是扫描所有代码,但是不执行。它创建作用域,并且为其作用域内的每个变量和函数都分配内存。之后还初始化 this 的值。

创建阶段会创建一个词法环境 LexicalEnvironment 组件和变量环境 VariableEnvironment 组件。在这个时候它们的值都相同。执行上下文的 LexicalEnvironment 和 VariableEnvironment 组件始终是 Lexical Environments。

因此,执行上下文的概念可以用以下代码表示:

ExecutionContext = { LexicalEnvironment = <ref. to LexicalEnvironment in memory>, VariableEnvironment = <ref. to VariableEnvironment in memory>, }

什么是 词法环境(Lexical Environments)

Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

中文理解就是词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。词汇环境由一个环境记录和一个可能为空的对外部词汇环境的引用组成。

词法环境就是一个链表结构 Lexical Environments -> outer -> global environment -> null

词法环境分为两种类型:

  1. 全局环境: 全局执行上下文,他没有外部环境的引用或者说为 null,在浏览器的环境下拥有一个全局对象 window 和关联的方法和属性: Math,String,Date 等。还有用户定义的全局变量,例如 var 定义的变量和全局定义的函数,并将 this 指向全局对象。

  2. 函数环境: 用户在函数定义的变量将储存在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。环境记录中包含用户声明的变量,函数,还有 arguments 对象。

// 词法环境可以用一下的伪代码表示 GlobalExectionContent = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 剩余标识符 }, Outer: null, } } FunctionExectionContent = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 剩余标识符 }, Outer: [Global or outer function environment reference], } }

什么是 环境记录(Environment Records)

环境记录是在词法环境中存放变量和函数声明的地方。

环境记录可分为两种类型:

  1. 声明性环境记录:声明性环境变量存储了作用域中的变量、常量、类、模块或函数声明。函数环境记录(Function Environment Records)也是一个声明性环境记录,用于表示函数的顶级范围,如果函数不是 ArrowFunction,则提供 this 绑定。

    通常情况下,声明式环境记录绑定的内容会被直接存储在底层实现上,如虚拟机的寄存器上,以便于快速访问。另外声明式环境记录项的特性允许使用完整的词法寻址技术,无需任何作用域链查找即可直接访问所需的变量。声明式环境记录除了支持可变绑定之外,还提供不可变绑定。不可变绑定是一种标识符和值之间的关联一旦建立就不能修改的绑定。如果绑定的内容是不可更改的,所有变量的地址在编译时就可以确定,这样 js 引擎在执行时就可以针对性进行优化。

  2. 对象环境记录:绑定了一组字符串标识符名称,这些名称直接对应于其绑定对象的属性名称。在浏览器环境下就是保存对象的key-value在 windows 上,所以对象环境记录不存在不可变绑定。

全局环境记录(Global Environment Records)

全局环境记录在逻辑上是单个记录,但它被指定为封装对象环境记录和声明性环境记录的组合。

全局环境记录的对象环境记录组件包含所有内置全局变量的绑定,以及由全局代码中包含的FunctionDeclarationGeneratorDeclarationAsyncFunctionDeclarationAsyncGeneratorDeclarationVariableStatement引入的所有绑定。全局代码中所有其他 ECMAScript 声明的绑定包含在全局环境记录的声明性环境记录组件中。

用伪代码的形式可以表现为以下的方式:

GlobalEnvironmentRecords: { outerEnv: null, // 全局环境 的外部引用为null [[GlobalThisValue]]: // this的执行 如 window [[ObjectRecord]]: { // 即对象式环境记录ObjectEnvironmentRecord // 包含了全局下var、function、generator、async声明的标识符 还有其他内置对象 如Math、Date // 用全局对象(如window)作为绑定对象,所以在全局下用var、function...声明的变量可以通过window[变量名] 访问(或window.变量名) [变量名]: undefined }, [[DeclarativeRecord]]:{ // 即声明式环境记录DeclarativeEnvironmentRecord // 除了var、function、generator、async声明的标识符保存在这里,如let、const [变量名]: uninitialized // 在编译阶段为uninitialized }, [[varNames]]: // var、function、generator、async声明的标识符列表 }

通过以下代码来

debugger; var a = 20; let b = 7; const e = 77; function c() { console.log('bar'); } const d = function () { console.log(777); };

由上图中的输出结果可以得知,var 声明的变量已经函数都挂载到了 Global 对象上,也就是 window 上,而 let,const 声明的变量和函数挂载到了 script 上。

跟着上面的 🌰,我们再举个 🌰

console.log(moment); // undefined var moment = '777'; console.log(foo); // ReferenceError: Cannot access 'foo' before initialization let foo = 18;

在上面的代码中,输出结果是可预测的,因为在 JavaScript 引擎的编译阶段会创建一个全局对象(Global)。

使用 var 声明的变量会挂载到全局对象上,因此在代码执行阶段,moment 属性已存在,只是值为 undefined,这就是我们常说的 “变量提升”。

但使用 let 声明的变量只有在代码执行阶段才创建。如果在声明之前访问,JavaScript 引擎会按照作用域链逐层查找,由于在声明之前找不到 foo,会抛出 ReferenceError

再有一个 🌰

var a; console.log(a); // undefined a = 1; var b; b = 2; console.log(b); // 2 先有蛋(声明),后有鸡(赋值) f(); // 111 依然是作用域提示 function f() { console.log(111); } var c; function c() { console.log('test'); } console.log(c, Object.prototype.toString.call(c)); function d() { console.log('test'); } var d; console.log(d, Object.prototype.toString.call(c));

根据上面的代码,我们可以知道函数声明和变量声明都会被提升。但是值得注意的细节是, 在重复声明的代码中, 函数会被首先提升, 然后才是变量。

什么是作用域

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的对变量进行赋值,那么就会使用 LHS 查询,如果目的是获取变量的值就会使用 RHS 查询。

作用域链就是通过作用域一层一层的往上找,直到查找到顶层(全局作用域),可能找到,也可能没找到,但无论如何查找过程都将终止,因为全局作用域外层作用域的指向为 null。

作用域分为两种主要的工作模型。第一种是词法作用域,另外一种叫作动态作用域。

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在代码时将变量和快作用域写在哪里决定的,作用域在编译的时候已经决定了,不会再修改。

而动态作用域是是通过运行时就行修改的,就是前面讲到的 eval 语法和 with 语法,这会导致性能下降。

作用域和执行上下文是什么关系

执行下上下文,简单概括来说就是全局代码执行期间,JS 引擎就会创建一个栈来存储管理所有的执行上下文对象。

作用域是在 JavaScript 源代码编译成机器码和字节码之前,编译器需要依赖的一个作用对象。用于查找当前环境所定义的变量等,最终输出整体抽象语法树(AST),作用域一旦确定便不会再修改了。

二者形似,但生不逢时。

最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666