第43课:JavaScript 语法基础与变量——var、let、const、提升与暂时性死区
JavaScript 的变量声明有三种关键字:var、let 和 const。表面上它们都是用来创建变量,但在作用域、提升行为、重复声明和可变性上存在本质差异。不理解这些差异,就会写出变量污染、意外覆盖、引用错误等难以调试的 Bug。本节课将逐一拆解三者的语法特性、底层机制(如创建阶段的提升和执行阶段的赋值),并建立正确的变量命名规范,为后续的函数、闭包、模块化学习打下坚实的语法基础。
1. var 关键字:函数作用域与变量提升
var 是 ES5 及之前唯一的变量声明方式,它有两个核心特征:函数作用域和声明提升。
1.1 var 的函数作用域
用 var 声明的变量,其作用域是最近的函数体(或全局作用域),而不是块级({})。这意味着在 if、for、while 等块内部用 var 声明的变量,在块外部仍然可以访问。
1 | function testVar() { |
这与大多数类 C 语言的行为不同。一个常见的陷阱是循环中的 var 泄漏到循环外部,并且循环内异步操作共享同一个变量:
1 | for (var i = 1; i <= 3; i++) { |
每次迭代的 setTimeout 回调都引用同一个 i,当回调执行时循环早已结束,i 的值已经变为 4。
1.2 var 的提升(Hoisting)
JavaScript 引擎在执行任何代码之前,会先扫描整个作用域,将 var 声明的变量提升到作用域顶部,并初始化为 undefined。因此,可以在声明语句之前访问该变量(不会报错,但值为 undefined)。
1 | function hoistingExample() { |
这相当于引擎将上述代码理解为:
1 | function hoistingExample() { |
重要:只有声明被提升,初始化(赋值)仍保留在原位置。函数声明整体提升,但 var 仅提升声明部分。
1.3 var 的重复声明
在同一个作用域内,使用 var 多次声明同一个变量不会报错,后面声明的语句会被忽略(仅相当于普通赋值)。这很容易造成意外覆盖。
1 | var count = 1; |
在大型代码库中,这种静默行为可能导致难以追踪的变量污染。
2. let 和 const:块级作用域与暂时性死区
ES6 引入了 let 和 const,解决了 var 的诸多痛点。它们都拥有块级作用域,并且受到暂时性死区(TDZ) 的保护。
2.1 块级作用域
let 和 const 声明的变量,其作用域是最近的块({} 包裹的区域),包括 if、for、while、switch 以及裸的 {}。
1 | function testLet() { |
循环中使用 let 会为每次迭代创建一个新的绑定,解决了 var 的异步共享问题:
1 | for (let i = 1; i <= 3; i++) { |
2.2 暂时性死区(Temporal Dead Zone, TDZ)
let 和 const 的声明也会被提升,但与 var 不同,它们在声明语句之前不能以任何方式访问(包括 typeof),否则抛出 ReferenceError。从块开始到声明语句之前的这段区域称为暂时性死区。
1 | function tdzExample() { |
即使是安全的 typeof 操作符也无法穿越 TDZ:
1 | { |
TDZ 的存在使得变量必须先声明后使用,这从根本上消除了变量提升带来的未定义行为。
2.3 let vs const:可变性差异
| 关键字 | 是否必须初始化 | 能否重新赋值 | 作用域 | 提升行为 | 重复声明 |
|---|---|---|---|---|---|
var |
否(默认 undefined) |
可以 | 函数 | 提升并初始化 | 允许 |
let |
否(但声明前不可访问) | 可以 | 块级 | 提升但不初始化 | 不允许 |
const |
是(必须立即赋值) | 不可以 | 块级 | 提升但不初始化 | 不允许 |
const 声明的变量必须声明时立即初始化,且之后不能重新指向另一个值。但需要注意:const 限制的是变量与值之间的绑定,而不是值本身。如果 const 指向一个对象或数组,那么该对象/数组内部的属性或元素是可以修改的。
1 | const user = { name: 'Alice' }; |
如果需要真正不可变的对象,应使用 Object.freeze()(浅冻结)或 Immutable 库。
2.4 let 和 const 的重复声明
在同一块级作用域内,重复声明同名 let 或 const 变量会引发 SyntaxError。这能有效防止意外覆盖。
1 | let x = 1; |
注意:不同块级作用域中可以声明同名变量,因为它们互不干扰。
1 | { |
3. 全局作用域下的差异
在全局作用域中,var 和 let/const 的行为还有一个关键差异:var 声明的全局变量会成为全局对象(浏览器中是 window,Node.js 中是 global)的属性,而 let/const 不会。
1 | <script> |
这一差异在模块化开发中影响不大(因为 ES Modules 默认严格模式,顶层变量不会泄漏到全局),但在传统的 <script> 脚本中需要注意:var 可能意外覆盖已有的全局属性(如 name、status、top 等)。
4. 变量命名规范
4.1 标识符规则
JavaScript 变量名(标识符)必须遵循以下规则:
- 首字符必须是字母、下划线
_或**美元符号$**。 - 后续字符可以是字母、数字、下划线、美元符号。
- 不能使用保留字(如
class、return、function、let、const等)。 - 区分大小写:
myVar和myvar是两个不同的变量。
1 | let _private = 'ok'; // ✅ 下划线开头 |
4.2 命名约定
JavaScript 社区形成了成熟的命名约定:
| 实体类型 | 约定 | 示例 |
|---|---|---|
| 变量 / 函数 | camelCase(驼峰) | userName、calculateTotal() |
| 常量(基本类型、配置) | UPPER_SNAKE | MAX_SIZE、API_BASE_URL |
| 类 / 构造函数 | PascalCase | UserModel、EventEmitter |
| 私有属性(约定) | 下划线前缀 _ |
this._internalState |
| 布尔变量 | 前缀 is/has |
isLoading、hasPermission |
语义化命名原则:变量名应准确描述其存储的值,避免单字母(除循环变量 i、j)和模糊缩写。
1 | // ❌ 糟糕的命名 |
5. 实战建议:选择 let 还是 const
在日常开发中,推荐以下策略:
- **默认使用
const**:任何不会重新赋值的变量都应用const。这传达了“此变量不可变”的意图,减少意外修改。 - **需要重新赋值时用
let**:如循环计数器、累加结果、状态标志。 - **完全避免
var**:在现代 JavaScript 代码中,var已无立足之地。它的函数作用域和提升行为是大量 Bug 的根源。若遇到遗留代码,需理解var的行为以便重构。
1 | // 推荐风格 |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在
for (var i = 0; i < 3; i++) { ... }循环外部,console.log(i)的输出是?
A.undefined
B.ReferenceError: i is not defined
C.3
D.0(单选) 关于暂时性死区(TDZ),以下描述正确的是?
A.var声明的变量也存在 TDZ。
B. TDZ 意味着变量在整个作用域中都不可访问。
C. 在let x声明之前访问x会抛出ReferenceError。
D. TDZ 是 JavaScript 引擎的缺陷,不需要关心。(填空)
const声明的变量必须在声明时进行______,且之后不能重新赋值。但它限制的是变量与值的______,而不是值本身的不可变性。(多选) 以下哪些是 JavaScript 合法的变量命名?
A._myVariable
B.$element
C.2ndPlace
D.user-name
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 工具函数,该函数接收一个数字数组,返回数组中的最大值。函数要求:
- 使用
const声明函数和不会变化的引用,使用let声明循环中的累加变量。 - 在函数内部使用块级作用域(
{})临时存储中间结果,演示块级作用域的隔离。 - 包含清晰的 JSDoc 注释,变量命名遵循 camelCase 规范。
- 提供一个使用示例,包括正常情况和空数组(返回
-Infinity或自定义默认值)。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定变量声明的选择(const 优先)、块级作用域的使用、以及命名规范。
课后练习答案
一、概念自测答案
C
- 解析:
var是函数作用域,循环结束后i变为 3,循环外仍可访问。
- 解析:
C
- 解析:
let和const存在 TDZ,声明前访问会抛出ReferenceError。var没有 TDZ,它会被初始化为undefined。
- 解析:
初始化 和 绑定(或引用)
- 解析:
const必须在声明时赋值(初始化),且绑定不可更改,但对象内部属性可修改。
- 解析:
A、B
- 解析:A 下划线开头合法;B 美元符开头合法;C 数字开头不合法;D 连字符不合法(会被解析为减法)。
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数findMax,接收一个数字数组,返回最大值。要求:
- 使用
const声明函数(函数表达式或函数声明)和不会变化的变量;循环中使用let声明累加变量。- 在函数内部使用一个块级作用域
{ ... }临时存储一个中间计算结果(例如:{ const tempMax = ...; }),然后在块外无法访问该变量。- 遵循 camelCase 命名规范,函数名
findMax,参数名numbers。- 添加 JSDoc 注释,说明参数类型、返回值类型和异常情况。
- 提供使用示例:
findMax([3, 7, 2, 9])返回9,findMax([])返回自定义默认值null或-Infinity(通过条件判断)。- 代码简洁,完全使用
const和let,无var。输出完整代码。”