菜单

作用域链

相关源文件

作用域链是 JavaScript 中确定变量如何在嵌套作用域中查找的基本机制。它代表了嵌套在其他作用域内的作用域之间的连接,并定义了可以访问变量的路径。理解作用域链对于掌握 JavaScript 中的词法作用域至关重要,也是闭包等高级概念的重要基础(有关闭包的更多信息,请参阅 闭包)。

目的与范围

本文档将解释

  • 作用域链如何连接嵌套作用域
  • 变量查找如何遍历作用域链
  • 变量遮蔽如何影响变量的可访问性
  • 函数名称和箭头函数在作用域链中的特殊情况

有关全局作用域在作用域链中的作用,请参阅 全局作用域周围

嵌套作用域和作用域链

作用域链之所以存在,是因为 JavaScript 允许作用域相互嵌套。当作用域嵌套时,它们会形成一种定向关系,即内部作用域可以访问其外部包含作用域中的变量,反之则不然。

来源: scope-closures/ch3.md17-18 scope-closures/ch2.md66-69

每个作用域完全包含在其父作用域中——作用域绝不会部分存在于两个不同的外部作用域中。这种嵌套创建了一个链状结构(作用域链),该结构决定了变量查找规则。

当内部作用域中的代码引用变量时,JavaScript 引擎首先检查当前作用域。如果在当前作用域中找不到变量,它会检查下一个外部作用域,一直向外查找,直到

  1. 在外部作用域中找到变量
  2. 到达全局作用域但未找到变量,从而导致 ReferenceError

来源: scope-closures/ch3.md19-27 scope-closures/ch2.md233-237

变量查找:概念与实际实现

虽然作用域链查找通常被解释为一种运行时过程(检查每个作用域直到找到变量),但这种概念模型与现代 JavaScript 引擎的实际实现不同。

概念查找过程

查找行为可以概念化如下:

  1. 遇到变量引用时,从当前作用域开始
  2. 如果未找到变量,则移至下一个外部作用域
  3. 继续查找,直到找到变量或到达全局作用域

实际实现

实际上,变量作用域信息主要在编译期间确定。

  1. 在编译过程中,JavaScript 引擎会确定每个变量属于哪个作用域。
  2. 这些信息存储在变量的抽象语法树 (AST) 条目中。
  3. 在运行时,引擎已经知道每个变量来自哪个作用域桶。
  4. 这种优化避免了通过作用域链进行运行时查找的需要。

有些情况仍然需要运行时查找。

  • 可能在其他文件中声明的未声明变量的引用
  • 需要为其他特殊情况在运行时解析的引用

来源: scope-closures/ch3.md20-48 scope-closures/ch2.md85-92

弹珠和桶的比喻

一个有助于可视化作用域链的方法是使用“弹珠和桶”的比喻。可以想象

  • 变量是彩色的弹珠
  • 作用域是匹配颜色的桶
  • 每个弹珠(变量)的颜色都基于它在哪一个桶(作用域)中声明。

来源: scope-closures/ch2.md12-21 scope-closures/ch3.md10-16

在这个比喻中,变量查找就像通过当前桶寻找弹珠。如果不在当前桶中,则检查下一个外部桶,依此类推,直到找到弹珠或检查完所有桶。

作用域链中的变量遮蔽

作用域链最重要的方面之一是它如何处理多个作用域包含同名变量的情况。

当内部作用域中的变量与外部作用域中的变量同名时,就会发生变量遮蔽。发生这种情况时,内部变量会“遮蔽”外部变量,使得该内部作用域无法访问外部变量。

来源: scope-closures/ch3.md50-88

变量遮蔽示例

当函数中的参数或变量与外部作用域中的变量同名时,函数内部对该名称的引用将始终指向内部变量,而不是外部变量。

在此示例中,由于变量遮蔽,函数内部对 studentName 的引用始终指向参数,而不是全局变量。

来源: scope-closures/ch3.md60-77

全局变量的遮蔽解除技巧

虽然被遮蔽的变量通常是不可访问的,但有一个特殊的技巧可以通过全局对象来访问已被遮蔽的全局变量。

此技巧仅适用于

  • 全局作用域中的变量
  • 使用 varfunction 声明的变量
  • 浏览器环境(其中 window 指向全局对象)

来源: scope-closures/ch3.md96-122

非法遮蔽

并非所有变量遮蔽的组合在 JavaScript 中都是允许的。具体来说:

  • let 可以遮蔽 var
  • var 不能遮蔽 let(跨越块边界)

发生这种情况是因为 var 声明是函数作用域的,并且会“跨越”块作用域的 let 声明的边界,而 JavaScript 不允许这样做。

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

函数名称在作用域链中的体现

函数声明将其名称标识符添加到其封闭作用域。但是,函数表达式在其名称方面表现不同。

具名函数表达式

使用具名函数表达式时,函数名称仅在函数自身作用域内可用。

在此示例中,askQuestion 被添加到外部作用域,但 ofTheTeacher 仅在函数内部可访问。

来源: scope-closures/ch3.md264-310

匿名函数表达式

匿名函数表达式不创建任何额外的名称标识符。

来源: scope-closures/ch3.md329-336

箭头函数和作用域链

ES6 箭头函数遵循与常规函数相同的词法作用域规则。尽管存在普遍的误解,但它们在作用域链方面的行为并无不同。

箭头函数始终是匿名的(没有直接相关的标识符),但它们仍然像常规函数一样创建独立的嵌套作用域。

来源: scope-closures/ch3.md342-386

作用域链可视化

让我们通过一个包含多个嵌套作用域的完整示例来可视化作用域链。

来源: scope-closures/ch2.md15-70 scope-closures/ch3.md10-16

该图展示了变量查找如何遍历作用域链。当在 for 循环中引用 students 时,查找从函数作用域开始,在该作用域中未找到,然后继续到定义它的全局作用域。同样,studentID 在函数作用域中找到,而 student 在循环的块作用域中找到。

结论

作用域链是 JavaScript 中决定嵌套作用域如何访问变量的关键机制。理解作用域链的规则对于编写可维护的 JavaScript 代码并避免与变量作用域相关的常见错误至关重要。

要记住的关键点

  1. 作用域链以定向关系连接嵌套作用域
  2. 变量查找从内部作用域移动到外部作用域(向上/向外)
  3. 内部作用域可以访问外部作用域中的变量,反之则不能
  4. 当内部作用域声明一个与外部作用域中的变量同名的变量时,就会发生变量遮蔽
  5. 作用域链主要在编译期间确定,而不是在运行时确定

作用域链构成了闭包和模块模式等其他高级 JavaScript 概念的基础。

来源: scope-closures/ch3.md387-393