菜单

词法作用域

相关源文件

词法作用域是 JavaScript 作用域系统的一个基本方面,它决定了如何在嵌套的代码结构中解析变量名。本文档解释了词法作用域的工作原理、它在编译阶段是如何确定的,以及它在 JavaScript 编程中的重要性。有关基于词法作用域概念的作用域链的信息,请参阅 作用域链。有关利用词法作用域的闭包的信息,请参阅 闭包

什么是词法作用域?

词法作用域(有时也称为静态作用域)是指变量的访问由源代码中变量和代码块的物理位置决定的作用域模型,而不是由运行时调用堆栈决定的。 “词法”一词指的是编译的词法分析(分词)阶段,即在执行代码之前对其结构进行分析。

在 JavaScript 中,词法作用域是变量解析的基础——它决定了在嵌套作用域中,一个引用指向哪个变量。

来源: scope-closures/ch1.md296-302 scope-closures/ch1.md1-9

编译与词法作用域

JavaScript 使用一个两阶段的处理模型

  1. 解析/编译阶段:代码被解析,词法作用域被建立
  2. 执行阶段:代码使用编译阶段定义的作用域结构进行执行

词法作用域主要是在编译阶段确定的,而不是在代码执行阶段。这意味着,变量和函数在代码中的物理位置决定了它们的作用域,即使在代码运行之前也是如此。

在编译过程中,JS 引擎会创建一个所有词法作用域的映射,这些作用域定义了程序在执行期间需要什么。作用域规则由函数、代码块和变量声明相对于彼此的物理位置决定。

来源: scope-closures/ch1.md19-23 scope-closures/ch1.md47-51 scope-closures/ch1.md78-81 scope-closures/ch1.md303-309

可视化模型:弹珠和桶

理解词法作用域的一个有用比喻是彩色弹珠被分拣到匹配的彩色桶中

  • 变量(弹珠)在特定的作用域(桶)中声明
  • 每个弹珠的颜色由其定义的桶(作用域)的颜色决定
  • 此确定过程在编译期间发生
  • 颜色信息用于执行期间的变量查找

图示:嵌套词法作用域的范围气泡

使用我们的比喻,我们可以可视化示例代码中的三个不同的作用域“气泡”或“桶”

  • 红色(最外层全局作用域):包含 studentsgetStudentNamenextStudent
  • 蓝色(函数作用域):包含 studentID 参数
  • 绿色(循环作用域):包含 student 变量

每个作用域都完全包含在其父作用域内。

来源: scope-closures/ch2.md12-26 scope-closures/ch2.md47-66 scope-closures/ch2.md76-78

变量查找是如何工作的

当代码中引用(但未声明)一个变量时,JavaScript 引擎必须确定它属于哪个作用域

  1. 从变量被引用的当前作用域开始
  2. 如果在当前作用域中找到变量,则使用它
  3. 如果未找到,则查找下一个更外层的包围作用域
  4. 继续向外移动通过嵌套作用域,直到找到为止
  5. 如果变量从未找到,则会抛出 ReferenceError,或者(在非严格模式下)意外地创建一个全局变量

图示:词法作用域解析过程

此查找过程的概念就像依次询问每个作用域管理器是否知道某个特定变量,从当前作用域开始向外移动,直到找到匹配项或到达全局作用域。

来源: scope-closures/ch2.md80-89 scope-closures/ch2.md224-237 scope-closures/ch3.md10-19

标识符及其角色

在分析词法作用域时,变量扮演两种角色之一

  1. 目标:当变量作为赋值操作的目标出现时
  2. 来源:当变量被引用以检索其值时
var students = [ /* ... */ ];  // "students" is a target
console.log(students);         // "students" is a source

编译器在编译和执行阶段以不同的方式处理这些角色

  • 编译期间:声明与其适当的作用域一起注册
  • 执行期间:根据作用域映射执行赋值和检索

来源: scope-closures/ch1.md197-202 scope-closures/ch1.md205-238 scope-closures/ch1.md249-256

遮蔽

遮蔽发生在当同一个变量名在不同的嵌套作用域中使用时。内部变量“遮蔽”了外部变量,使得外部变量在该内部作用域中无法访问。

图示:嵌套作用域中的遮蔽

当内部作用域中的变量与外部作用域中的变量同名时,它会创建一个新变量,而不是访问外部变量。在该内部作用域中对该变量名的任何引用都指向内部(遮蔽)变量,而不是外部(被遮蔽)变量。

来源: scope-closures/ch3.md50-93 scope-closures/ch3.md145-168

遮蔽的限制

并非所有声明遮蔽的组合都是允许的

  • let 可以遮蔽 var
  • 在同一个函数作用域内,var 不能遮蔽 let

禁止 var 遮蔽 let 的限制是,因为 var 声明是函数作用域的(不是块作用域),并且会尝试“跳过”或“跨越”块作用域的 let 声明的边界。

来源: scope-closures/ch3.md205-261

词法作用域与全局作用域

全局作用域是 JavaScript 程序中最外层的词法作用域。它充当

  1. JS 语言内置项的默认容器(undefinedArrayObject 等)
  2. 宿主环境的内置项(浏览器中的 consoledocument 等)
  3. 您自己全局定义的变量和函数

全局作用域是作用域链查找过程的最后一步。如果在任何嵌套的词法作用域中都找不到变量,JS 引擎将在判定变量未声明之前检查全局作用域。

来源: scope-closures/ch4.md10-11 scope-closures/ch4.md92-107

在运行时修改词法作用域(反模式)

虽然词法作用域通常在编译时确定,但在运行时(仅在非严格模式下)有两种已弃用的修改作用域的方法。

  1. eval() 函数:可以解析包含修改当前作用域的声明的代码。
  2. with 语句:可以将对象视为一个作用域。

强烈不推荐使用这些技术,并且在严格模式下已禁用,因为它们:

  • 使代码更难理解和调试。
  • 阻止 JS 引擎优化性能。
  • 造成潜在的安全漏洞。

来源: scope-closures/ch1.md264-294

词法作用域作为 JavaScript 功能的基础

词法作用域对几个重要的 JavaScript 概念至关重要:

  1. 闭包:函数即使在别处执行,也能保持对其词法作用域中变量的访问。
  2. 模块:模块模式使用词法作用域和闭包来创建私有实现细节。
  3. 块作用域letconst 利用块级别的词法作用域。

图示:词法作用域作为 JavaScript 功能的基础

理解词法作用域对于掌握这些更高级的 JavaScript 概念和模式至关重要。

来源: scope-closures/ch8.md4-11 scope-closures/ch8.md387-390

处理词法作用域的最佳实践

要在代码中有效利用词法作用域:

  1. 使用严格模式,以防止意外的全局变量和其他作用域相关问题。
  2. 将变量保持在最小的作用域内(最小暴露原则)。
  3. 避免使用 eval()with 在运行时修改作用域。
  4. 注意变量名可能出现的遮蔽(shadowing)问题。
  5. 有意识地使用函数表达式和块来创建有用的作用域边界。
  6. 了解代码将被如何编译,以确保预期的作用域行为。

遵循这些实践有助于创建更易于维护、更可预测的代码,并最大限度地减少与变量访问和可见性相关的问题排查。

来源: scope-closures/ch1.md290-294 scope-closures/ch2.md296-298 scope-closures/ch3.md125-126

结论

词法作用域是在编译时根据代码中编写变量和作用域块的位置确定的。这种静态作用域模型使 JavaScript 引擎能够优化变量访问,并构成了闭包和模块等重要 JavaScript 模式的基础。

当你在 JavaScript 中引用变量时,引擎会使用编译期间创建的词法作用域映射来解析你正在访问的变量,沿着作用域链从当前作用域向外查找,直到找到匹配项。

理解词法作用域对于编写精确、可维护的 JavaScript 代码以及有效利用语言的高级功能至关重要。

来源: scope-closures/ch1.md296-309 scope-closures/ch3.md388-393