JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用

作者:木棉时间:2026-11-05 21:20:30浏览:206

深入解析JavaScript函数中的变量提升(Hoisting)与暂时性死区(TDZ),通过多道经典测试题详解var、let与const在函数作用域和块级作用域下的执行生命周期与避坑指南。

语言核心机制 作用域与闭包 阅读时间:约 12 分钟
在 JavaScript 函数中,变量在代码编写位置与实际解析时机之间存在明显的阶段差。理解 var 的变量提升与 let、const 的暂时性死区(TDZ),是掌握变量可见性、解决闭包陷阱与避免运行时 ReferenceError 的关键。本文直接拆解函数执行上下文中的底层生命周期,并通过 4 道经典测试题逐行推导执行过程。
JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
图 0:函数作用域中 var 提升与 let/const 暂时性死区对比总览

一、核心机制:函数执行上下文中的变量生命周期

当 JavaScript 引擎调用一个函数时,会在调用栈中为该函数压入一个全新的执行上下文(Execution Context)。执行上下文的运行严格划分为两个阶段:创建阶段(Creation Phase)与执行阶段(Execution Phase)。

1. 变量声明的三大阶段

任何变量在 JavaScript 引擎内部的注册都要经历以下三个独立环节:

  • 创建阶段(Declaration / Binding):在当前词法环境或变量环境中登记标识符名称。
  • 初始化阶段(Initialization):为变量分配初始内存空间。在此阶段完成前,变量不可被读取。
  • 赋值阶段(Assignment):计算表达式并将实际的业务数据写入已分配的内存中。

2. var 与 let / const 的本质差异

变量提升(Hoisting)并非代码在物理上被移动到了函数顶部,而是引擎在执行上下文创建阶段提前完成了标识符登记。关键的区别在于初始化的触发时机:

  • var 的提升机制:在上下文创建阶段,var 同时完成了“创建”与“初始化”,并被强制赋予初始值 undefined。因此,在函数体内声明语句之前读取 var 变量,程序不会中断,而是直接返回 undefined。
  • let / const 与暂时性死区(TDZ):在创建阶段,let 和 const 仅完成了“创建”绑定,初始化步骤被推迟到代码逐行执行到对应的声明语句时才触发。从进入当前作用域块开始,直到执行到实际声明语句之间的代码区域,被称为暂时性死区(Temporal Dead Zone, TDZ)。在此区域内任何尝试读取或写入该标识符的操作,引擎都会直接抛出 ReferenceError。
  • const 的专属限制:const 要求创建、初始化与赋值必须在声明语句处原子完成,不允许延迟赋值,且后续禁止修改引用的内存地址。
JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
图 1:JavaScript 函数执行上下文与变量生命周期推导流程

二、经典测试题逐行拆解与运行推导

以下 4 道测试题覆盖了前端面试与工程开发中最容易产生认知偏差的典型场景,包含形参覆盖、局部遮蔽、块级作用域逃逸以及循环闭包捕获。

测试题 1 同名形参、var 声明与函数声明在函数内部的优先级竞争

请观察以下函数代码,推导调用 test(10) 后的控制台输出:

function test(a) {
  console.log("第 1 处输出:", a);
  var a = 20;
  function a() {}
  console.log("第 2 处输出:", a);
}
test(10);
运行结果输出:
// 第 1 处输出: [Function: a]
// 第 2 处输出: 20

逐行推导与原理分析:

  1. 进入执行上下文创建阶段:
    首先,处理形参 a = 10,在变量环境中建立绑定并赋值 10。
    其次,扫描函数声明 function a() {}。函数声明具有高优先级,由于名称同为 a,函数声明会直接覆盖之前的形参赋值,此时 a 指向该函数对象。
    最后,扫描 var a = 20。因为变量环境中已经存在标识符 a,var a 的声明部分被直接忽略(不会重置为 undefined)。
  2. 进入代码执行阶段:
    执行第 2 行:此时 a 仍然是创建阶段确立的函数对象,因此第一处打印 [Function: a]。
    执行第 3 行:a = 20 执行赋值表达式,变量 a 被重新赋予数值 20。
    执行第 4 行:function a() {} 属于函数声明,已在创建阶段处理完毕,执行阶段直接跳过。
    执行第 5 行:输出此时的数值 20。
测试题 2 暂时性死区遮蔽(Shadowing)与默认参数陷阱

请判断以下两个案例的执行结果:

// 案例 A:作用域遮蔽引发的 TDZ
let x = "outer";
function checkShadow() {
  console.log(x);
  let x = "inner";
}
checkShadow();

// 案例 B:函数默认参数互相引用
function checkParams(a = b, b = 2) {
  return a + b;
}
checkParams();
运行结果输出:
// 案例 A 输出:
// ReferenceError: Cannot access 'x' before initialization

// 案例 B 输出:
// ReferenceError: Cannot access 'b' before initialization

逐行推导与原理分析:

案例 A 分析:初学者常误以为由于外层存在 let x = "outer",内部第一句会输出 "outer"。然而,在 checkShadow 函数被调用时,其内部作用域在创建阶段就已检测到 let x = "inner",并在局部词法环境中登记了 x 的绑定。根据 JavaScript 词法作用域检索规则,当前作用域只要存在该标识符,解析立即锁定当前作用域,不会向外层回溯。由于执行尚未到达赋值行,局部 x 位于 TDZ 内,读取操作直接抛出 ReferenceError。

案例 B 分析:函数的形参列表拥有属于自己的独立参数作用域。参数求值是严格从左到右依次进行的。当计算 a = b 时,变量 b 虽然已在参数作用域中登记,但尚未执行到 b = 2 的初始化阶段,因此 b 处于参数 TDZ 中,访问引发 ReferenceError。

JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
图 2:词法作用域检索与暂时性死区遮蔽(Shadowing)机理
测试题 3 条件分支与块级嵌套中的 var 作用域逃逸

请推导以下函数在传入不同参数时的执行逻辑:

function conditionScope(flag) {
  console.log("块前访问:", num);
  if (flag) {
    var num = 100;
    let blockVal = 200;
  }
  console.log("块后访问 num:", num);
  console.log("块后访问 blockVal:", typeof blockVal);
}
conditionScope(false);
运行结果输出:
// 块前访问: undefined
// 块后访问 num: undefined
// 块后访问 blockVal: undefined

逐行推导与原理分析:

  1. var num 没有块级作用域概念,它会被提升到包裹它的整个函数 conditionScope 作用域顶部,并初始化为 undefined。因此,即使代码在 if 分支之前,或者 if 条件为 false 根本不执行,num 依然存在于函数环境中,值为 undefined。
  2. let blockVal 严格遵循 if (flag) { ... } 建立的块级作用域。当 flag 为 false 时,该块级环境甚至没有被激活;即便条件为 true,该变量也在块执行结束后立即销毁。在块外部执行 typeof blockVal 时,由于外界从未声明过该变量,返回 "undefined",不会报错。
测试题 4 循环闭包中的经典变量捕获(var 共享 vs let 迭代绑定)

请推导以下分别使用 var 和 let 的循环函数数组执行结果:

// 循环 A:使用 var
var listA = [];
for (var i = 0; i < 3; i++) {
  listA.push(function() { return i; });
}
console.log(listA[0](), listA[1](), listA[2]());

// 循环 B:使用 let
var listB = [];
for (let j = 0; j < 3; j++) {
  listB.push(function() { return j; });
}
console.log(listB[0](), listB[1](), listB[2]());
运行结果输出:
// 循环 A 输出: 3 3 3
// 循环 B 输出: 0 1 2

逐行推导与原理分析:

在循环 A 中,var i 是外层作用域中的同一个全局/函数变量。在循环的 3 次迭代中,所有推入 listA 的闭包函数都保持着对同一个变量 i 的引用。循环终止时,i 的最终状态为 3。当外部主动调用这些函数时,它们都从同一内存槽中读取数据,因而全为 3。

在循环 B 中,ECMAScript 规范对 for (let ...) 做了专门的设计:每一次迭代都会生成一个全新的块级词法环境。在进入下一轮迭代时,引擎会把上一轮迭代结束时的值作为初始值复制到新的块级环境中。因此,每个闭包函数各自捕获并锁定了属于自己轮次的 j 实例(分别是 0、1、2),最终输出符合直觉的递增数字。

JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
图 3:循环闭包中 var 与 let 的内存绑定模型对比

三、规避陷阱的最佳实践与特性速查

在实际工程开发中,理解提升与死区的最终目标是写出健壮、可维护、无副作用的代码。遵循以下三条准则可完全杜绝相关隐患:

工程黄金法则:
  1. 首选 const,次选 let,完全禁用 var:利用块级作用域限制变量生命周期,借助 TDZ 机制在编码早期暴露未初始化引用的逻辑错误。
  2. 变量在作用域顶部集中声明:尽量在使用变量前完成初始化,避免依赖提升行为。
  3. 默认参数保持独立:避免后方默认参数依赖前方未确认的参数,避免复杂的参数互引。
JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
图 4:var、let 与 const 全维度语法与运行时特性总结表

通过理解函数执行上下文的创建与执行阶段分界、死区的本质(已绑定但未初始化),以及 let 在块作用域中的独立性,开发者能够清晰预判每一行 JavaScript 代码的求值顺序与内存状态。

作者最新文章
相关文章
更多