WinddSnow

JavaScript-Syntax-Basics-Variables-var-let-const-Hoisting-TDZ

字数统计: 3k阅读时长: 11 min
2026/07/29

第43课:JavaScript 语法基础与变量——varletconst、提升与暂时性死区

JavaScript 的变量声明有三种关键字:varletconst。表面上它们都是用来创建变量,但在作用域提升行为重复声明可变性上存在本质差异。不理解这些差异,就会写出变量污染、意外覆盖、引用错误等难以调试的 Bug。本节课将逐一拆解三者的语法特性、底层机制(如创建阶段的提升和执行阶段的赋值),并建立正确的变量命名规范,为后续的函数、闭包、模块化学习打下坚实的语法基础。


1. var 关键字:函数作用域与变量提升

var 是 ES5 及之前唯一的变量声明方式,它有两个核心特征:函数作用域声明提升

1.1 var 的函数作用域

var 声明的变量,其作用域是最近的函数体(或全局作用域),而不是块级({})。这意味着在 ifforwhile 等块内部用 var 声明的变量,在块外部仍然可以访问。

1
2
3
4
5
6
7
function testVar() {
if (true) {
var message = 'Hello';
}
console.log(message); // 'Hello' —— 在 if 块外仍然可访问
}
testVar();

这与大多数类 C 语言的行为不同。一个常见的陷阱是循环中的 var 泄漏到循环外部,并且循环内异步操作共享同一个变量:

1
2
3
4
5
6
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i); // 输出 4, 4, 4 —— 循环结束后 i 变为 4
}, 0);
}
console.log('循环结束后 i =', i); // 4 —— 变量泄漏到 for 外部

每次迭代的 setTimeout 回调都引用同一个 i,当回调执行时循环早已结束,i 的值已经变为 4。

1.2 var 的提升(Hoisting)

JavaScript 引擎在执行任何代码之前,会先扫描整个作用域,将 var 声明的变量提升到作用域顶部,并初始化为 undefined。因此,可以在声明语句之前访问该变量(不会报错,但值为 undefined)。

1
2
3
4
5
6
function hoistingExample() {
console.log(name); // undefined —— 变量已提升,但尚未赋值
var name = 'Alice';
console.log(name); // 'Alice'
}
hoistingExample();

这相当于引擎将上述代码理解为:

1
2
3
4
5
6
function hoistingExample() {
var name = undefined; // 提升:声明被提到顶部,默认 undefined
console.log(name);
name = 'Alice';
console.log(name);
}

重要:只有声明被提升,初始化(赋值)仍保留在原位置。函数声明整体提升,但 var 仅提升声明部分。

1.3 var 的重复声明

在同一个作用域内,使用 var 多次声明同一个变量不会报错,后面声明的语句会被忽略(仅相当于普通赋值)。这很容易造成意外覆盖。

1
2
3
var count = 1;
var count = 2; // 不报错,count 变为 2
console.log(count); // 2

在大型代码库中,这种静默行为可能导致难以追踪的变量污染。


2. letconst:块级作用域与暂时性死区

ES6 引入了 letconst,解决了 var 的诸多痛点。它们都拥有块级作用域,并且受到暂时性死区(TDZ) 的保护。

2.1 块级作用域

letconst 声明的变量,其作用域是最近的块{} 包裹的区域),包括 ifforwhileswitch 以及裸的 {}

1
2
3
4
5
6
7
8
function testLet() {
if (true) {
let message = 'Hello';
const PI = 3.14;
}
console.log(message); // ReferenceError: message is not defined
}
testLet();

循环中使用 let 会为每次迭代创建一个新的绑定,解决了 var 的异步共享问题:

1
2
3
4
5
6
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i); // 1, 2, 3 —— 每次迭代都有自己的 i
}, 0);
}
// console.log(i); // ReferenceError: i is not defined —— 不会泄漏

2.2 暂时性死区(Temporal Dead Zone, TDZ)

letconst 的声明也会被提升,但与 var 不同,它们在声明语句之前不能以任何方式访问(包括 typeof),否则抛出 ReferenceError。从块开始到声明语句之前的这段区域称为暂时性死区

1
2
3
4
5
6
7
function tdzExample() {
// TDZ 开始
console.log(name); // ReferenceError: Cannot access 'name' before initialization
// TDZ 结束
let name = 'Bob';
}
tdzExample();

即使是安全的 typeof 操作符也无法穿越 TDZ:

1
2
3
4
{
console.log(typeof value); // ReferenceError(而不是 'undefined')
let value = 10;
}

TDZ 的存在使得变量必须先声明后使用,这从根本上消除了变量提升带来的未定义行为。

2.3 let vs const:可变性差异

关键字 是否必须初始化 能否重新赋值 作用域 提升行为 重复声明
var 否(默认 undefined 可以 函数 提升并初始化 允许
let 否(但声明前不可访问) 可以 块级 提升但不初始化 不允许
const (必须立即赋值) 不可以 块级 提升但不初始化 不允许

const 声明的变量必须声明时立即初始化,且之后不能重新指向另一个值。但需要注意:const 限制的是变量与值之间的绑定,而不是值本身。如果 const 指向一个对象或数组,那么该对象/数组内部的属性或元素是可以修改的。

1
2
3
4
5
6
7
8
9
const user = { name: 'Alice' };
user.name = 'Bob'; // 合法:修改对象属性,没有改变 user 的引用
console.log(user); // { name: 'Bob' }

// user = { name: 'Charlie' }; // TypeError: Assignment to constant variable.

const numbers = [1, 2, 3];
numbers.push(4); // 合法
console.log(numbers); // [1, 2, 3, 4]

如果需要真正不可变的对象,应使用 Object.freeze()(浅冻结)或 Immutable 库。

2.4 letconst 的重复声明

在同一块级作用域内,重复声明同名 letconst 变量会引发 SyntaxError。这能有效防止意外覆盖。

1
2
3
4
5
let x = 1;
// let x = 2; // SyntaxError: Identifier 'x' has already been declared

const y = 10;
// const y = 20; // 同上

注意:不同块级作用域中可以声明同名变量,因为它们互不干扰。

1
2
3
4
5
6
7
8
{
let value = 'outer';
{
let value = 'inner'; // 合法:不同的块作用域
console.log(value); // 'inner'
}
console.log(value); // 'outer'
}

3. 全局作用域下的差异

全局作用域中,varlet/const 的行为还有一个关键差异:var 声明的全局变量会成为全局对象(浏览器中是 window,Node.js 中是 global)的属性,而 let/const 不会。

1
2
3
4
5
6
7
8
9
<script>
var globalVar = 'I am var';
let globalLet = 'I am let';
const globalConst = 'I am const';

console.log(window.globalVar); // 'I am var'
console.log(window.globalLet); // undefined
console.log(window.globalConst); // undefined
</script>

这一差异在模块化开发中影响不大(因为 ES Modules 默认严格模式,顶层变量不会泄漏到全局),但在传统的 <script> 脚本中需要注意:var 可能意外覆盖已有的全局属性(如 namestatustop 等)。


4. 变量命名规范

4.1 标识符规则

JavaScript 变量名(标识符)必须遵循以下规则:

  • 首字符必须是字母下划线 _ 或**美元符号 $**。
  • 后续字符可以是字母、数字、下划线、美元符号。
  • 不能使用保留字(如 classreturnfunctionletconst 等)。
  • 区分大小写:myVarmyvar 是两个不同的变量。
1
2
3
4
5
let _private = 'ok';     // ✅ 下划线开头
let $element = 'ok'; // ✅ 美元符开头(常用于 jQuery 或变量引用 DOM 元素)
let user123 = 'ok'; // ✅ 字母 + 数字
// let 123abc = 'no'; // ❌ 数字开头
// let my-var = 'no'; // ❌ 连字符

4.2 命名约定

JavaScript 社区形成了成熟的命名约定:

实体类型 约定 示例
变量 / 函数 camelCase(驼峰) userNamecalculateTotal()
常量(基本类型、配置) UPPER_SNAKE MAX_SIZEAPI_BASE_URL
类 / 构造函数 PascalCase UserModelEventEmitter
私有属性(约定) 下划线前缀 _ this._internalState
布尔变量 前缀 is/has isLoadinghasPermission

语义化命名原则:变量名应准确描述其存储的值,避免单字母(除循环变量 ij)和模糊缩写。

1
2
3
4
5
6
7
8
9
// ❌ 糟糕的命名
let d; // 什么日期?
let flag = false; // 什么标志?
let arr = []; // 什么数组?

// ✅ 清晰的命名
let createdAt; // 创建日期
let isFormValid = false; // 表单是否有效
let activeUsers = []; // 活跃用户列表

5. 实战建议:选择 let 还是 const

在日常开发中,推荐以下策略:

  • **默认使用 const**:任何不会重新赋值的变量都应用 const。这传达了“此变量不可变”的意图,减少意外修改。
  • **需要重新赋值时用 let**:如循环计数器、累加结果、状态标志。
  • **完全避免 var**:在现代 JavaScript 代码中,var 已无立足之地。它的函数作用域和提升行为是大量 Bug 的根源。若遇到遗留代码,需理解 var 的行为以便重构。
1
2
3
4
5
6
7
// 推荐风格
const PI = 3.14159;
const config = { apiUrl: '/api' }; // 对象引用不变
let counter = 0;
for (let i = 0; i < 10; i++) {
counter += i;
}

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选)for (var i = 0; i < 3; i++) { ... } 循环外部,console.log(i) 的输出是?
    A. undefined
    B. ReferenceError: i is not defined
    C. 3
    D. 0

  2. (单选) 关于暂时性死区(TDZ),以下描述正确的是?
    A. var 声明的变量也存在 TDZ。
    B. TDZ 意味着变量在整个作用域中都不可访问。
    C. 在 let x 声明之前访问 x 会抛出 ReferenceError
    D. TDZ 是 JavaScript 引擎的缺陷,不需要关心。

  3. (填空) const 声明的变量必须在声明时进行 ______,且之后不能重新赋值。但它限制的是变量与值的 ______,而不是值本身的不可变性。

  4. (多选) 以下哪些是 JavaScript 合法的变量命名?
    A. _myVariable
    B. $element
    C. 2ndPlace
    D. user-name

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要编写一个 JavaScript 工具函数,该函数接收一个数字数组,返回数组中的最大值。函数要求:

  • 使用 const 声明函数和不会变化的引用,使用 let 声明循环中的累加变量。
  • 在函数内部使用块级作用域({})临时存储中间结果,演示块级作用域的隔离。
  • 包含清晰的 JSDoc 注释,变量命名遵循 camelCase 规范。
  • 提供一个使用示例,包括正常情况和空数组(返回 -Infinity 或自定义默认值)。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定变量声明的选择(const 优先)、块级作用域的使用、以及命名规范。


课后练习答案

一、概念自测答案

  1. C

    • 解析:var 是函数作用域,循环结束后 i 变为 3,循环外仍可访问。
  2. C

    • 解析:letconst 存在 TDZ,声明前访问会抛出 ReferenceErrorvar 没有 TDZ,它会被初始化为 undefined
  3. 初始化绑定(或引用

    • 解析:const 必须在声明时赋值(初始化),且绑定不可更改,但对象内部属性可修改。
  4. A、B

    • 解析:A 下划线开头合法;B 美元符开头合法;C 数字开头不合法;D 连字符不合法(会被解析为减法)。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请编写一个 JavaScript 函数 findMax,接收一个数字数组,返回最大值。要求:

  • 使用 const 声明函数(函数表达式或函数声明)和不会变化的变量;循环中使用 let 声明累加变量。
  • 在函数内部使用一个块级作用域 { ... } 临时存储一个中间计算结果(例如:{ const tempMax = ...; }),然后在块外无法访问该变量。
  • 遵循 camelCase 命名规范,函数名 findMax,参数名 numbers
  • 添加 JSDoc 注释,说明参数类型、返回值类型和异常情况。
  • 提供使用示例:findMax([3, 7, 2, 9]) 返回 9findMax([]) 返回自定义默认值 null-Infinity(通过条件判断)。
  • 代码简洁,完全使用 constlet,无 var。输出完整代码。”
CATALOG
  1. 1. 第43课:JavaScript 语法基础与变量——var、let、const、提升与暂时性死区
    1. 1.1. 1. var 关键字:函数作用域与变量提升
      1. 1.1.1. 1.1 var 的函数作用域
      2. 1.1.2. 1.2 var 的提升(Hoisting)
      3. 1.1.3. 1.3 var 的重复声明
    2. 1.2. 2. let 和 const:块级作用域与暂时性死区
      1. 1.2.1. 2.1 块级作用域
      2. 1.2.2. 2.2 暂时性死区(Temporal Dead Zone, TDZ)
      3. 1.2.3. 2.3 let vs const:可变性差异
      4. 1.2.4. 2.4 let 和 const 的重复声明
    3. 1.3. 3. 全局作用域下的差异
    4. 1.4. 4. 变量命名规范
      1. 1.4.1. 4.1 标识符规则
      2. 1.4.2. 4.2 命名约定
    5. 1.5. 5. 实战建议:选择 let 还是 const
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)