var / let / const 区别

0 阅读8分钟

摘要varletconst 是 JS 中三种变量声明关键字,核心区别集中在 作用域(Scope)声明提升(Hoisting)暂时性死区(TDZ) 三个维度。一句话记忆:var 是老古董函数作用域会挂 window,let/const 块级作用域有 TDZ,默认用 const 需要改才 let,永远别用 var

var_let_const_作用域与提升行为对比.png

一、核心区别速查表

特性varletconst
作用域 (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:块级作用域

letconst 的作用域是最近的 {}(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) 提升导致意外的 undefinedvar 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 图解见正文。