let、const 和 var 有什么区别?
简化版
var 是函数作用域,有变量提升,可以重复声明;let 和 const 是块级作用域,有暂时性死区,不能在同一作用域重复声明。const 只保证绑定不能重新赋值,不保证对象内容不可变。
详细版
主要区别:
- 作用域:
var是函数作用域,let/const是块级作用域。 - 提升:三者都会被提升,但
let/const在声明前访问会触发暂时性死区错误。 - 重复声明:
var允许,let/const不允许。 - 赋值:
const声明时必须初始化,之后不能重新绑定。
const obj = { a: 1 };
obj.a = 2; // 可以
obj = {}; // 报错
因为 const 锁的是变量绑定,不是对象内部属性。工程里优先用 const,需要重新赋值时用 let,尽量不用 var。
完整版教学
一、var 的函数作用域问题
var 不认块级作用域:
if (true) {
var a = 1;
}
console.log(a); // 1
这在循环和异步回调里很容易出错。let 会把变量限制在块内,让代码更符合直觉。
二、暂时性死区是什么
let/const 声明的变量在作用域开始时已经存在,但在执行到声明语句前不能访问,这段区域叫暂时性死区。
console.log(a); // ReferenceError
let a = 1;
它的价值是减少“先使用后声明”的混乱代码。
三、const 和不可变不是一回事
const 表示变量名不能再指向另一个值。如果值是对象,对象内部仍然可以修改。
要让对象更接近不可变,可以用 Object.freeze,但它默认只冻结第一层。真正不可变数据通常需要约定、工具库或结构共享方案。
四、最佳实践
优先 const 能表达“这个绑定不会变”,让读代码的人更放心。只有确实需要重新赋值,比如循环计数、状态累积时才用 let。var 主要出现在老代码里,新代码尽量避免。
五、面试追问与工程落地
面试官常会追问“let/const 有没有变量提升”。更准确的答案是:有提升,但不能在声明前访问,因为处于暂时性死区。不要简单说“let 不提升”,这会被继续追问。声明在词法环境中已经被创建,只是初始化前不可用。
还可能问循环里的 let 为什么能解决异步输出问题。for (let i...) 每轮循环都会创建新的词法绑定,所以每个回调捕获的是不同的 i;var 只有一个函数级 i,回调共享同一个变量。
工程里 const 优先并不是为了对象不可变,而是为了表达绑定不变,减少读代码时的心智负担。如果后面不重新赋值,就用 const;需要累加、重新指向、状态切换时再用 let。
六、从环境记录解释作用域与循环闭包
var 声明绑定在函数或全局 var 环境中,let、const 则绑定在当前词法环境中。进入块级作用域时,词法绑定已经存在但尚未初始化,从作用域起点到声明执行之间就是暂时性死区;这不是“变量不存在”,而是访问未初始化绑定会抛出 ReferenceError。
const jobs = []
for (let i = 0; i < 3; i++) {
jobs.push(() => i)
}
console.log(jobs.map(fn => fn())) // [0, 1, 2]
for 循环使用 let 时,每轮迭代会创建对应的词法绑定,因此 3 个闭包分别捕获 0、1、2。改用 var 时通常只有 1 个共享绑定,循环结束后值为 3,三个闭包都会读到 3。
| 对比项 | var | let | const |
|---|---|---|---|
| 作用域 | 函数/全局 | 块级 | 块级 |
| 声明前访问 | undefined | TDZ 报错 | TDZ 报错 |
| 同作用域重复声明 | 允许部分情况 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
浏览器经典脚本顶层的 var 可能成为全局对象属性,而顶层 let、const 不会;ES 模块又拥有自己的模块作用域。讨论“全局变量”时必须先说明代码运行在经典脚本、模块还是函数包装环境。
const保证的是绑定不能指向另一个值,不会递归冻结对象;需要不可变数据时还要配合设计约束或冻结方案。
七、常见误区与追问
- 误区:let 和 const 不会变量提升。 声明会建立绑定,只是在初始化前处于暂时性死区,不能提前访问。
- 误区:const 声明的对象完全不可变。 不能给变量重新赋值,但对象内部属性仍可修改。
- 误区:var 只有“可以重复声明”这一个问题。 它还缺少块级作用域,并可能造成循环闭包共享同一绑定。
- 追问:为什么 typeof x 在 TDZ 中也会报错?
typeof只有对未解析标识符较特殊;已经存在但未初始化的词法绑定仍触发ReferenceError。 - 追问:循环中的 let 为什么能得到不同值? 每轮迭代会建立新的对应绑定,闭包捕获的不是同一个共享变量。
- 追问:const 为什么必须初始化? 绑定一旦初始化就不能重新赋值,因此普通声明必须同时给出初值。
- 追问:日常代码如何选择三者? 默认使用
const,确需重新赋值时用let,新代码通常不使用var。
八、加强记忆
这题记住三句话:var 管函数,let/const 管块;let/const 有暂时性死区;const 锁绑定,不锁对象内容。
答题时先区分绑定的创建与初始化,再说明作用域、TDZ、重赋值和每轮循环绑定,结论会更完整。 不要只背“优先 const”,还要能解释这种选择如何缩小可变状态范围。