摘要:
var、let、const是 JS 中三种变量声明关键字,核心区别集中在 作用域(Scope)、声明提升(Hoisting)、暂时性死区(TDZ) 三个维度。一句话记忆:var 是老古董函数作用域会挂 window,let/const 块级作用域有 TDZ,默认用 const 需要改才 let,永远别用 var。
一、核心区别速查表
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 (Scope) | 函数作用域 (Function Scope) | 块级作用域 (Block Scope) | 块级作用域 (Block Scope) |
| 变量提升 (Hoisting) | 声明提升,初始化为 undefined | 声明提升,但不初始化 | 声明提升,但不初始化 |
| 暂时性死区 (TDZ) | 无 | 有 | 有 |
| 重复声明 | 允许在同一作用域重复声明 | 不允许在同一作用域重复声明 | 不允许在同一作用域重复声明 |
| 修改值 | 可以修改 | 可以修改 | 不可以修改基本类型的值或重新赋值(对象属性/数组元素可修改) |
| 全局对象属性 | 在顶层作用域声明会成为 window 的属性 | 在顶层作用域声明不会成为 window 的属性 | 在顶层作用域声明不会成为 window 的属性 |
二、作用域(Scope)详解
2.1 var:函数作用域
var 的作用域是整个函数,不受 {} 块的限制:
// === var 的函数作用域 ===
function example() {
if (true) {
var x = 10; // var 不受 if 块限制
}
console.log(x); // 10 ✅(在函数内任何地方都能访问)
}
example();
console.log(x); // ReferenceError! ✅(不泄漏到全局)
// 但在全局声明时:
var globalVar = 'hello';
console.log(window.globalVar); // 'hello' ❌ 污染了全局对象!
2.2 let / const:块级作用域
let 和 const 的作用域是最近的 {} 块(if/for/while/function/block):
// === let/const 的块级作用域 ===
function example() {
if (true) {
let y = 20; // let 受 if 块限制
const z = 30; // const 同样受块限制
}
console.log(y); // ReferenceError! y is not defined ❌
console.log(z); // ReferenceError! z is not defined ❌
}
// 循环中的经典差异:
for (var i = 0; i < 3; i++) { // var:i 是函数作用域
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(循环结束后 i=3,三个回调共享同一个 i)
for (let j = 0; j < 3; j++) { // let:j 是块级作用域,每次迭代新绑定
setTimeout(() => console.log(j), 100);
}
// 输出:0, 1, 2(每次迭代 j 都是独立的值)
2.3 全局声明的行为差异
// === 顶层声明的区别 ===
var a = 1;
let b = 2;
const c = 3;
console.log(window.a); // 1 ✅(var 挂载到 window)
console.log(window.b); // undefined(let 不挂载)
console.log(window.c); // undefined(const 不挂载)
// 这意味着 var 可能覆盖已有的全局属性!
var navigator = 'oops'; // 覆盖了 window.navigator!❌ 危险
三、变量提升(Hoisting)详解
3.1 什么是提升
JS 引擎在执行代码前会先进行编译阶段(parser → transformer),将所有变量和函数声明"提到"当前作用域的顶部。这就是提升(Hoisting)。
┌──────────────────────────────┐
│ 执行上下文 │
│ ┌────────────────────┐ │
│ │ 词法环境 │ │ ← 存放 let/const/function
│ │ (Lexical Env) │ │
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │ 变量环境 │ │ ← 存放 var
│ │ (Variable Env) │ │
│ └────────────────────┘ │
└──────────────────────────────┘
编译流程: 源代码 → [tokenizer] → AST → [transformer] → 执行
3.2 var 的提升:声明 + 初始化为 undefined
// === 实际执行顺序(var)===
console.log(a); // undefined(不是报错!)
var a = 10;
console.log(a); // 10
// JS 引擎实际理解为:
var a; // ← 提升到顶部,初始化为 undefined
console.log(a); // undefined
a = 10; // ← 赋值留在原位
console.log(a); // 10
3.3 let / const 的提升:声明但不初始化(TDZ)
// === 实际执行顺序(let/const)===
console.log(b); // ReferenceError! Cannot access before initialization ❌
let b = 20;
// JS 引擎实际理解为:
let b; // ← 声明提升到顶部,但不初始化!(进入 TDZ)
console.log(b); // ❌ 还在 TDZ 中,访问报错
b = 20; // ← 赋值时才初始化,离开 TDZ
console.log(b); // 20
3.4 函数声明的特殊提升
// === 函数声明:整体提升(声明+定义一起提)===
console.log(fn1()); // 'hello' ✅(函数定义也提升了)
function fn1() { return 'hello'; }
// === 函数表达式:只有变量名提升 ===
console.log(fn2()); // TypeError: fn2 is not a function ❌
var fn2 = function() { return 'world'; };
// 实际:var fn2 = undefined → fn2() 报 TypeError(不是 ReferenceError)
// === let 函数表达式:TDZ 报 ReferenceError ===
console.log(fn3()); // ReferenceError ❌
let fn3 = function() { return '!'; };
四、暂时性死区(TDZ)详解
4.1 什么是 TDZ
暂时性死区(Temporal Dead Zone):从代码块的开始到 let/const 声明语句之间的区域。在这个区域内,变量虽然已被声明(存在绑定),但尚未初始化,访问它会抛出 ReferenceError。
// === TDZ 可视化 ===
{
// ══════════════════════════════════════
// ⚠️ TDZ 开始(x 进入死区)
// ══════════════════════════════════════
console.log(x); // ReferenceError! 还在 TDZ 中 ❌
// 一些其他代码...
let y = 5; // y 的 TDZ 结束
console.log(x); // 还在 TDZ 中!❌
let x = 10; // ★ x 初始化 —— TDZ 结束 ★
console.log(x); // 10 ✅
// ══════════════════════════════════════
// TDZ 结束,x 正常可用
// ══════════════════════════════════════
}
4.2 TDZ 的隐蔽陷阱
// === 陷阱 1:typeof 也不安全 ===
{
console.log(typeof notDeclared); // "undefined"(未声明变量不报错)
console.log(typeof inTDZ); // ReferenceError! ❌ 即使在 typeof 中也不行
let inTDZ = 1;
}
// === 陷阱 2:函数参数默认值中的 TDZ ===
function foo(x = y, y = 2) { // y 在自己的 TDZ 中被 x 的默认值引用
return x + y;
}
foo(); // ReferenceError! y 在 TDZ 中 ❌
function bar(x = 2, y = x) { // ✅ x 已经初始化
return x + y;
}
bar(); // 4
// === 陷阱 3:解构中的 TDZ ===
let x = x; // ReferenceError! x 在赋值右侧时还在 TDZ ❌
let { a = b, b = 1 } = {}; // ReferenceError! b 在 TDZ ❌
let { a = 1, b = a } = {}; // ✅ a 已初始化
4.3 为什么需要 TDZ?
TDZ 不是"bug",而是有意设计的安全机制:
- 防止在声明前使用变量(避免
undefined的静默错误) - 让程序的行为更可预测
- 帮助尽早发现编码错误
// 没有 TDZ(var 行为)的隐患:
var status = 'active';
// ... 很久之后 ...
if (condition) {
console.log(status); // 期望输出 'active'
var status = 'inactive'; // 但实际上这里输出 undefined!因为 var 提升了
// 这种 bug 极难排查
}
// 有 TDZ(let 行为):直接报错,立刻发现
let status = 'active';
if (condition) {
console.log(status); // ReferenceError! 立刻告诉你有问题 ✅
let status = 'inactive';
}
五、const 的特殊性
5.1 不可重新赋值 ≠ 不可变
// === const 的真正含义:绑定不可变,值不一定不可变 ===
// 基本类型:完全不可变
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable ❌
// 对象:引用地址不可变,但内容可以改
const user = { name: 'Alice', age: 25 };
user.name = 'Bob'; // ✅ 合法!修改的是对象内容
user.age = 26; // ✅ 合法
user = {}; // ❌ 非法!不能重新赋值(改变引用地址)
// 数组同理
const arr = [1, 2, 3];
arr.push(4); // ✅ 合法
arr[0] = 99; // ✅ 合法
arr = []; // ❌ 非法!
// 如果要真正的不可变对象:
const frozen = Object.freeze({ name: 'Alice' });
frozen.name = 'Bob'; // 静默失败(严格模式下 TypeError)
5.2 const 必须立即初始化
const x; // SyntaxError: Missing initializer in const declaration ❌
let y; // ✅ let 可以不初始化(值为 undefined)
六、完整对比与最佳实践
6.1 决策树
要声明一个变量?
│
├─ 这个变量的值会被重新赋值吗?
│ ├─ 是 → 用 let
│ │ 例:循环计数器、状态标志、DOM 元素引用
│ │
│ └─ 否 → 用 const
│ 例:配置常量、导入模块、回调函数、不变引用
│
└─ 会用 var 吗?
└─ 绝对不用!ES6+ 项目中 var 已被淘汰
6.2 最佳实践总结
| 规则 | 说明 |
|---|---|
| ① 默认用 const | 保证数据不可变性,代码更可预测,V8 对 const 有更多优化空间 |
| ② 需要重赋值时用 let | 循环计数器、需要改变状态的变量 |
| ③ 永远不用 var | 函数作用域 + 提升(undefined) + 挂 window = bug 温床 |
| ④ 变量在使用前声明 | 利用 TDZ 的保护,避免意外行为 |
| ⑤ 一个声明一个变量 | const a = 1; const b = 2; 比 const a = 1, b = 2; 更清晰 |
6.3 实际项目示例
// === 推荐写法 ===
import React from 'react'; // const(导入不变)
const API_BASE = 'https://api.example.com'; // const(配置常量)
function UserList({ users }) { // const(组件不变)
const [list, setList] = useState([]); // const(useState 返回的引用不变)
let page = 1; // let(会递增)
useEffect(() => {
const fetchData = async () => { // const(回调不变)
const res = await fetch(API_BASE); // const(请求结果不变)
const data = await res.json(); // const
setList(data);
};
fetchData();
}, []);
const handleLoadMore = () => { // const(回调不变)
page += 1; // let 重新赋值 ✅
// ...加载更多
};
return <div>{/* JSX */}</div>;
}
七、面试高频 Q&A
Q1:var、let、const 最本质的区别是什么?
A:最本质的区别是作用域——var 是函数作用域(在整个函数内可见),let/const 是块级作用域(只在 {} 内可见)。由此衍生出提升行为的差异:var 提升并初始化为 undefined(所以访问不报错),let/const 提升但不初始化(形成 TDZ,访问报 ReferenceError)。const 额外要求必须初始化且不可重新赋值。
Q2:什么是暂时性死区(TDZ)?为什么设计它?
A:TDZ 是从代码块开始到 let/const 声明语句之间的区域。在这个区域内变量已绑定但未初始化,任何访问都会抛出 ReferenceError。设计目的是防止变量在声明前被使用,避免 var 导致的"隐式 undefined"这类难以排查的 bug。连 typeof 都无法绕过 TDZ。
Q3:const 声明的对象真的不可变吗?
A:不是。const 保证的是绑定(binding)不可变——即变量名指向的内存地址不能变。但对象内部的属性和数组元素仍然可以被修改(push、赋值属性等)。如果需要真正的不可变对象,要用 Object.freeze() 或使用 Immer 等库。
Q4:为什么说 var 是很多 bug 的根源?举具体例子。
A:三大罪状:(1) 函数作用域导致循环问题 — for(var i=0;...) 中 setTimeout 全输出同一个值;(2) 提升导致意外的 undefined — var x = 1 在 if 块内提升后覆盖外部同名变量;(3) 污染全局对象 — var name = 'x' 会变成 window.name,可能覆盖浏览器 API。这三个问题 let/const 都不存在。
Q5:函数声明 vs 函数表达式的提升有什么不同?
A:函数声明(function fn(){})整体提升(声明+定义一起移到顶部),可以在定义前调用;函数表达式(var fn = function(){} 或 let fn = function(){})只提升变量名,var 版本提升后为 undefined(调用报 TypeError),let 版本进入 TDZ(调用报 ReferenceError)。
八、记忆口诀
- var 函数域,let 块级域:var 整个函数都能访问,let 只在 {} 内有效
- var 提 undefined,let 提进 TDZ:var 提升后默认 undefined,let 提升后在死区里访问就报错
- const 绑定不可变,对象内容还能改:地址锁死,属性随便
- 默认 const 改用 let,永远别碰老 var:现代 JS 开发铁律
- TDZ 保护你提前发现 bug:比静默的 undefined 强一万倍
本笔记由「前端面试学习笔记」skill 生成,配套 SVG 图解见正文。