JavaScript函数作用域与块级作用域的区别:var与let/const对比

作者:闭包旅人

时间:2026-10-30

浏览:228

深入解析JavaScript中函数作用域与块级作用域的底层机制差异,全面对比var、let与const在声明提升、暂时性死区、循环迭代以及全局对象挂载方面的核心表现。

文章目录

在 JavaScript 中,作用域决定了标识符的访问范围与生命周期。ES6 之前,语言仅具备全局作用域与由 function 开辟的函数作用域,以 var 声明的变量会穿透普通代码块;ES6 引入的 let 与 const 则确立了块级作用域,把变量的可见性收敛在一对花括号 {} 之间。理解二者在作用域边界、变量提升机制、暂时性死区(TDZ)及循环绑定上的底层差异,是编写健壮、无泄漏现代 JavaScript 代码的基础。

一、作用域边界的核心机制:函数边界与代码块边界

JavaScript 引擎在执行代码前,会先进行编译与词法分析。在词法分析阶段,变量根据其声明关键字被绑定至不同的词法环境(Lexical Environment)或变量环境(Variable Environment)。

使用 var 声明的变量,其生存空间被锁定在距离其最近的外层函数或全局上下文中。这意味着由 if、for、while 或独立的 {} 构成的代码块,无法在语法层面限制 var 的可见性。变量会直接穿透这些代码块,挂载到外层函数作用域中,造成变量污染与意料之外的共享。

相反,let 和 const 将任何一对花括号 {} 视作独立的作用域屏障。一旦变量在块内声明,其外部无论如何都无法直接触达该变量,从而避免了局部变量逃逸至全局或上层函数的问题。

function testScope() {
  if (true) {
    var functionScoped = "我属于函数作用域";
    let blockScoped = "我属于块级作用域";
    const constBlockScoped = "我也属于块级作用域";
  }

  // var 能够穿透 if 代码块,正常输出
  console.log(functionScoped); // 输出: "我属于函数作用域"

  // let 与 const 被严格限制在 if 块内,块外访问直接报错
  // console.log(blockScoped);      // 抛出: ReferenceError: blockScoped is not defined
  // console.log(constBlockScoped); // 抛出: ReferenceError: constBlockScoped is not defined
}
testScope();
JavaScript函数作用域与块级作用域的区别:var与let/const对比
图 1:函数作用域与块级作用域边界隔离机制对比

二、声明提升(Hoisting)与暂时性死区(TDZ)的推导流程

很多开发者误以为“只有 var 会发生声明提升,而 let/const 不会提升”。从 ECMAScript 规范的实际运作过程来看,这种理解是不准确的。

在进入作用域创建执行上下文(Execution Context)时,JavaScript 引擎会分为两个阶段:编译阶段(登记)与执行阶段(求值)。所有的变量声明都会在编译阶段被引擎捕获并登记到当前作用域中:

  • var 的两步合一:登记标识符的同时,引擎会立即为其在内存中分配空间并赋予默认初值 undefined。因此在代码书写位置之前访问它,不会崩溃,而是读取到 undefined。
  • let / const 的分离机制:引擎虽然登记了标识符名称,但严禁进行内存初始化。从块级作用域开始,直到变量声明语句正式执行完成之前的这一段代码执行区间,被称为暂时性死区(Temporal Dead Zone,简称 TDZ)。在 TDZ 内访问该变量,引擎会抛出 ReferenceError。
// 场景 A: var 的声明提升
console.log(numVar); // 输出: undefined (已创建并初始化为 undefined)
var numVar = 100;
console.log(numVar); // 输出: 100

// 场景 B: let 的暂时性死区
// console.log(numLet); // 抛出: ReferenceError: Cannot access 'numLet' before initialization
let numLet = 200;    // 此时才完成初始化与赋值
console.log(numLet); // 输出: 200
JavaScript函数作用域与块级作用域的区别:var与let/const对比
图 2:变量提升与暂时性死区(TDZ)的执行推导流程

三、循环迭代中的作用域陷阱与排查解决步骤

作用域差异在异步回调和事件监听场景中表现得尤为明显。最典型的场景就是 for 循环内包含异步操作(如 setTimeout 或 DOM 点击事件)。

经典问题复现

执行 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } 时,控制台并不会依次输出 0、1、2,而是连续打印出三次 3。这是由于 var i 没有块级作用域,整个循环迭代共用同一个变量实例。当异步回调开始执行时,循环早已结束,i 的最终值已累加为 3。

排查并修复该问题的解决流程如下:

JavaScript函数作用域与块级作用域的区别:var与let/const对比
图 3:循环变量异步污染的排查、过渡与现代重构流程

四、重复声明与全局对象属性挂载

除了作用域边界与提升方式,var、let 和 const 在语法严格度及全局上下文处理上也存在显著差别:

1. 重复声明控制

在同一作用域内,var 允许重复声明同一个变量名。后出现的声明会被静默解析为赋值操作,不会引发任何语法错误。这种静默宽容常常导致团队协作中意外覆盖同名变量。而 let 与 const 具备词法唯一性校验,在同一作用域内重复声明同名标识符会直接抛出 SyntaxError: Identifier has already been declared,阻断潜在命名冲突。

2. 全局对象的污染问题

在全局作用域下执行代码时:

  • 使用 var 声明的全局变量,会被自动挂载为全局对象(浏览器环境下的 window,Node.js 下的 global)的属性。这很容易无意中覆盖原生全局 API(如 window.name 或 window.status)。
  • 使用 let 或 const 声明的全局变量,虽然在整个脚本内均可访问,但它们被保存在一个独立的 Script 词法环境中,绝不会挂载到全局对象上,保持了全局命名空间的纯净。
// 全局环境测试
var globalVar = "挂载到全局对象";
let globalLet = "保存在 Script 词法环境";

console.log(window.globalVar); // 输出: "挂载到全局对象"
console.log(window.globalLet); // 输出: undefined
console.log(globalLet);        // 正常输出: "保存在 Script 词法环境"

五、var、let 与 const 全维度特性矩阵与选型策略

在深入理解底层作用域与执行机制后,我们可以将三个关键字的关键维度归纳如下表:

特性对比维度 var let const
作用域类型 函数作用域 / 全局 块级作用域 块级作用域
声明提升表现 提升并预赋 undefined 登记符号,严禁初始化 登记符号,严禁初始化
暂时性死区 (TDZ) 不存在 TDZ 存在 TDZ(声明前访问抛错) 存在 TDZ(声明前访问抛错)
同一作用域重复声明 允许(静默覆盖) 禁止(抛出 SyntaxError) 禁止(抛出 SyntaxError)
重新赋值 允许 允许 禁止(抛出 TypeError)
初始值要求 可选(默认为 undefined) 可选(默认为 undefined) 必须显式赋予初值
全局对象挂载 (window) 挂载为属性 不挂载 不挂载
JavaScript函数作用域与块级作用域的区别:var与let/const对比
图 4:现代 JavaScript 变量声明关键字选型决策模型

六、实践总结与注意事项

通过深入剖析,我们可以总结出以下明确的开发与重构原则:

  1. 默认全面使用 const:让数据流尽量趋于可预测。声明为 const 的标识符不仅能防止意外重赋值,也向代码阅读者传达了“该标识符绑定不会变更”的明确语义。
  2. 仅在确认需要重新赋值时使用 let:例如数值循环计数器(for (let i = 0; ...))、状态机当前状态切换、累加运算等场景。
  3. 警惕 const 的“浅不可变性”:const 锁定的仅仅是变量与内存地址之间的引用绑定,并不代表对象内部属性不可更改。如果需要深度冻结对象属性,需配合 Object.freeze() 实现。
  4. 彻底剔除 var:遗留项目在向现代工程迁移时,应通过构建工具与代码检查规则(如 ESLint 的 no-var 规则)逐步消除所有 var 声明,消除历史作用域穿透带来的隐患。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章