专栏文章 JavaScript 中 var 的陷阱

FunTester · 2025年10月17日 · 最后由 卡丁车卡丁丁 回复于 2025年10月17日 · 375 次阅读

在 JavaScript 面试或笔试中,经常会出现类似下面这样的题目,看似简单但非常经典,考察的是对作用域和变量提升的理解。示例代码如下:


function test() {
// 在函数作用域内声明并初始化 x 为 1
var x = 1;
if (true) {
  // 这里再次使用 var 声明 x,但 var 是函数作用域,
  // 这行其实是在给同一个函数作用域内的 x 重新赋值为 2
  var x = 2;
  console.log(x); // 输出 2:此时 x 的值已被覆盖为 2
}
console.log(x); // 输出 2:外层仍能访问到被覆盖后的同一个 x
}

test(); // 调用函数,依次打印两次 2

很多人第一眼会以为 if 块内的 var x 是新的局部变量,因此希望看到输出为 2 1。但实际运行结果是:

2
2

本文将从语言特性、执行流程、对比现代声明方式和编码建议几方面系统讲解,帮助读者厘清常见误区并在实际编码中避免类似问题。

误区的根源: var 不是块级作用域

在 JavaScript 中,var 声明的变量具有函数作用域(function scope),而不是块作用域(block scope)。这意味着无论在 ifforwhile 等代码块内使用 var,变量都会被提升到包含它的函数体的顶端,并且在函数内部仅存在一份同名变量。对于初学者或从其他语言(如 Java、C++)转过来的人,容易将 var 误认为是块级作用域,从而误判变量生存期和覆盖关系。

更具体地说,JavaScript 引擎在函数执行前会进行一个 “声明登记” 过程:遇到 var 声明会在函数范围内注册一个变量名,但不会立即赋值(默认值为 undefined)。因此,函数在运行时访问该变量若未被赋值,返回的会是 undefined 而不是抛错。由此产生的常见现象包括:变量提升导致的意外 undefined、在循环或条件语句中覆盖外层变量、以及重复声明不会报错但会引入潜在的维护风险。理解 var 的这一本质行为,是解释该题目输出的关键。

执行流程图解

把代码的执行顺序拆解开来看,更容易理解发生了什么。以示例函数为例,实际在引擎内部的等价处理如下:


function test() {
  // 编译/登记阶段:var x 的绑定已在函数开始处创建(初始值 undefined)
  var x; // 绑定已存在,但尚未赋值
  x = 1; // 执行阶段:将 x 赋值为 1

  if (true) {
    // 在此处对同一绑定进行重新赋值
    x = 2; // 将函数作用域内的 x 更新为 2
    console.log(x); // 输出 2:当前绑定的值为 2
  }

  console.log(x); // 输出 2:同一个函数作用域内的绑定已被修改
}

可以将关键步骤按序列化说明:1) 声明阶段在函数开始时登记变量名;2) 执行阶段从上到下赋值与执行语句;3) if 内的 var x = 2; 并没有新建一个局部块变量,而是对函数范围内已存在的 x 重新赋值;4) 因此两次 console.log 打印的是同一个变量的当前值。把这些步骤逻辑化、条理化讲清楚,能帮助读者避免只凭直觉判断作用域的错误。

如果换成 let 或 const 呢?

将示例中的 var 替换为 letconst,行为会发生明显变化。例如:


function test() {
// 使用 let 创建块级作用域的变量 x,并初始化为 1
let x = 1;
if (true) {
  // 这里的 let x 是一个全新的、仅在 if 块内有效的绑定
  let x = 2;
  console.log(x); // 输出 2:访问的是 if 块内的局部 x
}
console.log(x); // 输出 1:访问的是外层函数作用域的 x,未被 if 内的 x 覆盖
}

test(); // 调用函数,按预期打印 2 和 1

此时输出为:

2
1

原因在于 letconst 具有块级作用域(block scope)。if 块内部的 x 是一个独立的变量,生命周期仅限于该块的 {} 范围,不会覆盖外层函数作用域中的 x。此外,let / const 不发生传统意义上的 “变量提升” 到可被访问的阶段(虽然会在内部创建绑定,但在初始化之前属于暂时性死区,访问会抛出 ReferenceError),这提升了变量使用的安全性,避免了 undefined 的尴尬输出和一些难以追踪的覆盖问题。因此在现代 JavaScript 开发中,推荐使用 let / const 以获得更可预测的作用域行为。

延伸:变量提升(Hoisting)的本质

“变量提升” 并非魔法,理解其本质有助于排查各种奇怪现象。JavaScript 在进入函数执行前,会进行一次静态扫描,登记所有使用 var 声明的变量名,并将这些变量的绑定建立在函数作用域中,但不会赋实际的运行值,默认值为 undefined。因此类似以下代码:


function demo() {
  // 由于 var 声明会在函数开始时被登记(提升),
  // 在此处访问 a 不会报错,但值为 undefined(尚未执行赋值语句)
  console.log(a); // undefined
  var a = 10; // 现在在执行阶段将 a 赋值为 10
}

demo(); // 调用 demo,首行打印 undefined

不会抛出错误,而是打印 undefined。这是因为引擎等价地把 var a 提升到函数顶端,再在当前位置赋值。相比之下,如果使用 letconst,在变量被声明之前访问会触发暂时性死区(Temporal Dead Zone),抛出 ReferenceError,从而显式暴露了未初始化访问的问题。理解两阶段模型(编译/登记阶段 + 执行阶段)能帮助开发者写出更稳健、易读的代码,并在定位 bug 时迅速判断是否与提升机制相关。

写代码的建议

基于对作用域与提升机制的理解,给出实践层面的建议以降低维护成本和潜在错误风险:1) 在现代项目中尽量避免使用 var,优先使用 const 声明不可变引用,必要时使用 let 声明可变变量;2) 将变量的作用域尽量限定到使用它的最小范围,尽量在需要处声明并初始化,而不是提前在函数顶端声明全局可见的变量;3) 使用静态分析工具(如 ESLint)并启用推荐规则集,可以自动识别出重复声明、不安全的提升使用等问题;4) 对于循环或异步回调中需要闭包捕获变量的场景,优先采用 let 以获得块级隔离,或显式使用闭包函数封装当前值;5) 在团队规范中明确禁止或限制 var 的使用,并通过代码评审与 CI 规则强制执行。遵循这些实践能显著提升代码可维护性并减少生产问题发生的概率。

总结归纳

关键要点如下:1) var:函数作用域、声明会被登记(提升),同函数内仅存在一份绑定,容易被重写;2) let / const:块级作用域、在初始化前属于暂时性死区,访问会报错,从而增强了安全性;3) 题目答案为 A) 2 2,这是由于 var 的函数作用域和赋值覆盖行为导致;4) 推荐实践:使用 const / let、保持作用域最小化、启用静态分析规则并在团队中统一编码规范。


FunTester 原创精华
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 1 条回复 时间 点赞

话说现在还有用这个的吗?
还是这文章是复制的以前的?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册