菜单

作用域与闭包

相关源文件

本文档涵盖了 JavaScript 中词法作用域和闭包的核心概念——这是组织、访问和保留代码中变量的基本机制。这些概念对于理解 JavaScript 的行为以及编写有效、可维护的代码至关重要。本材料对应于“You Don't Know JS Yet”系列的第二本书。

有关更广泛的 JavaScript 语言基础知识,请参阅概述。有关 JavaScript 类型系统的具体信息,请参阅类型与语法

什么是作用域?

作用域是决定变量在程序中何处以及如何被访问的一组规则。JavaScript 使用词法作用域,这意味着作用域是在编译时根据变量和作用域块在代码中的书写位置来确定的。

从本质上讲,JavaScript 的处理包含两个阶段:

  1. 编译阶段:确定作用域
  2. 执行阶段:根据先前确定的作用域规则访问变量

理解这种双阶段处理对于掌握 JavaScript 如何处理变量至关重要。

来源:scope-closures/ch1.md17-21 scope-closures/ch1.md77-81

词法作用域图示

词法作用域可以被可视化为一系列嵌套的气泡或容器,每个都代表一个变量所在的范围。

此图展示了书中示例的嵌套作用域结构。每个矩形都是一个包含自身变量的作用域“气泡”

  • 全局作用域包含 studentsgetStudentNamenextStudent
  • 函数作用域包含 studentID 参数
  • 循环作用域包含 student 循环变量

当 JavaScript 引擎需要查找变量时,它会从当前作用域开始,如果找不到则向外查找——这个过程称为“作用域查找”。

来源:scope-closures/ch2.md15-47 scope-closures/ch2.md50-56 scope-closures/ch2.md75-98

作用域链

当作用域嵌套时,它们会形成一个链条,JavaScript 引擎在查找变量时会遍历这个链条。

作用域链的工作方式如下:

  1. 引擎首先在代码当前执行的直接作用域中查找
  2. 如果找不到,它会查找下一个外部包含作用域
  3. 这个过程会一直持续,直到找到变量或者到达全局作用域
  4. 如果在任何作用域中都找不到该变量,则会抛出 ReferenceError

此查找过程是单向的,始终沿着作用域链向上/向外移动,一旦找到第一个匹配的变量就会停止。

来源:scope-closures/ch3.md18-19 scope-closures/ch2.md213-237

遮蔽

当链中的多个作用域存在相同的变量名时,内部变量会“遮蔽”外部变量,使得外部变量在该内部作用域中无法访问。

在这个例子中,参数 studentName 遮蔽了全局的 studentName,创建了两个具有相同名称但在不同作用域中的独立变量。

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

变量生命周期

了解变量在其作用域中何时可用,对于编写可预测的 JavaScript 代码至关重要。

提升 (Hoisting)

提升(Hoisting)是一种行为,在编译过程中,变量和函数的声明被概念上地“移动”到其包含作用域的顶部。

不同的声明类型被提升的方式不同:

  1. 函数声明会被提升并初始化为其函数值
  2. var 声明会被提升并自动初始化为 undefined
  3. letconst 声明会被提升,但不会被初始化(TDZ)

这解释了为什么函数可以在其声明出现在代码之前被调用。

来源:scope-closures/ch5.md11-33 scope-closures/ch5.md91-103

暂时性死区 (TDZ)

使用 letconst 声明时,变量在作用域开始到声明被处理的这段时间内,存在于所谓的“暂时性死区”中。

在 TDZ 期间,尝试访问变量会导致 ReferenceError,即使变量实际上“存在”于作用域中。

来源:scope-closures/ch5.md147-155 scope-closures/ch1.md129-151

闭包

闭包是 JavaScript 最强大的特性之一。当一个函数“记住”并访问外部作用域中的变量,即使该作用域已执行完毕,就会发生这种情况。

闭包的关键特征

  1. 闭包是维护对其词法作用域中变量访问权限的函数
  2. 即使在外部函数完成执行后,闭包仍然存在
  3. 闭包持有对变量的实时链接,而不仅仅是值的快照
  4. 每个函数实例都维护着其引用的变量的闭包

闭包是许多 JavaScript 模式的基础,包括回调、事件处理程序和模块。

来源:scope-closures/ch7.md14-27 scope-closures/ch7.md67-105 scope-closures/ch7.md126-131

常见的闭包模式

闭包在 JavaScript 中被广泛用于:

  • 数据隐私
  • 工厂函数
  • 回调函数
  • 事件处理程序
  • 模块模式

来源:scope-closures/ch7.md108-126 scope-closures/ch7.md152-175 scope-closures/ch7.md296-336

模块模式

模块模式是一种基于闭包的设计模式,它提供了一种封装私有实现细节,同时公开公共 API 的方法。

模块的特征

  1. 封装:将相关数据和行为分组
  2. 信息隐藏:允许私有实现细节
  3. 公共 API:仅公开外部使用所必需的内容
  4. 状态保持:通过闭包在函数调用之间维护私有状态

来源: scope-closures/ch8.md10-22 scope-closures/ch8.md98-149

模块格式

JavaScript has evolved several formats for implementing modules

Classic Module (Revealing Module)

CommonJS (Node.js)

ES Modules (ESM)

来源: scope-closures/ch8.md191-201 scope-closures/ch8.md202-281 scope-closures/ch8.md282-306

Scope Best Practices

Following the "Principle of Least Exposure" (POLE) is a key best practice for working with scope in JavaScript. This means limiting the accessibility of variables to only the parts of the code that need them.

Benefits of Following POLE

  1. Avoiding Variable Collision: Prevents accidental variable name conflicts
  2. Preventing Unexpected Behavior: Reduces bugs from unintended variable modifications
  3. Limiting Dependencies: Makes code more maintainable and refactorable

Practical Implementation

  • Use function scope for function-wide variables
  • Use block scope with let/const for variables needed only within specific blocks
  • Use IIFEs (Immediately Invoked Function Expressions) to create private scopes
  • Structure modules to hide implementation details

来源: scope-closures/ch6.md8-39 scope-closures/ch6.md177-189 scope-closures/ch6.md217-246

Global Scope Considerations

The global scope serves several important purposes in JavaScript programs

  1. It's where built-in language features like Object, Array, and undefined are defined
  2. It provides the environment's APIs like document in browsers or process in Node.js
  3. It can provide a shared space for code from multiple files

However, the global scope behaves differently across environments

Understanding these environment differences is crucial for writing portable JavaScript.

来源: scope-closures/ch4.md12-114 scope-closures/ch4.md122-156 scope-closures/ch4.md278-306

总结

Scope and closure are fundamental concepts in JavaScript that control variable visibility and lifetime. Through lexical scope, JavaScript determines at compile time which variables are accessible where. Closure extends this mechanism, allowing functions to maintain access to their lexical scope even when executed elsewhere.

Understanding these concepts enables you to

  1. Structure code with appropriate variable visibility
  2. Avoid common pitfalls like name collisions
  3. Create powerful patterns like modules for code organization
  4. Write more maintainable, predictable JavaScript code

The principles covered in this document form the foundation of the first pillar of JavaScript: the scope and closure system.

来源: scope-closures/ch1.md14-22 scope-closures/ch8.md9-13