深入理解 JavaScript 的执行原理:从变量提升到暂时性死区
你是否曾经疑惑:为什么可以在函数声明之前调用它?为什么用
var声明的变量在赋值前访问得到的是undefined而不是报错?而用let声明的变量却会抛出ReferenceError?这一切的背后,都源于 JavaScript 在运行前的“编译阶段”所做的工作。
一、变量提升(Hoisting)现象
先来看几个常见的例子:
javascript
// 1.js
showName(); // 输出:函数showName被执行了
console.log(myname); // 输出:undefined
console.log(add); // 输出:undefined
var myname = '极客时间';
function showName() {
console.log('函数showName被执行了');
}
var add = function(x, y) {
return x + y;
}
上面这段代码并没有报错。showName() 正常执行,myname 和 add 输出的是 undefined 而不是变量的真实值。
如果我们对比传统认知中“代码按顺序执行”的预期,会发现两个“奇怪”的现象:
- 在变量定义之前使用它:不会报错,但该变量的值是
undefined,而不是定义时的值。 - 在函数定义之前使用它:不会出错,且函数能够正确执行。
另外,如果使用了从未声明的变量,则会报错:
javascript
console.log(notDefined); // ReferenceError: notDefined is not defined
这些现象说明:JavaScript 代码并不是一行一行顺序执行的。在执行之前,还有一个重要的阶段——编译阶段。
二、编译阶段与执行阶段
JavaScript 虽然是脚本语言、弱类型、动态解释执行,但它并没有独立的编译步骤。编译发生在代码运行前的那一刹那:
- 全局代码执行前 → 编译 1 次(创建全局执行上下文)
- 每次函数被调用时 → 编译 1 次(创建函数执行上下文)
- 函数被调用 N 次 → 就会编译 N 次
输入一段代码,经过编译后,会生成两部分内容:
- 执行上下文(Execution Context) :代码执行所需的环境,包含变量环境、词法环境等。
- 可执行代码:编译后待执行的代码。
三、什么是变量提升?
变量提升是指在 JavaScript 代码执行过程中,引擎把变量的声明部分和函数的声明部分“提升”到代码开头的“行为”。变量提升后,引擎会给变量设置默认值,这个默认值就是 undefined。
编译阶段的具体工作
javascript
// 原始代码
showName();
console.log(myName);
function showName() {
var a = 1;
console.log('函数showName被执行了');
}
在编译阶段,引擎会为当前作用域准备好变量环境:
- 找到所有变量声明(
var),在环境中创建该变量并初始化为undefined。 - 找到所有函数声明(
function开头),在环境中创建该函数名并指向函数对象。
上面的代码经过编译后,相当于生成了这样的“执行上下文”:
javascript
// 编译阶段后的变量环境(伪代码)
var myName = undefined;
function showName() { ... } // 函数对象已就绪
而可执行代码部分则是剩下的语句:
javascript
showName();
console.log(myName);
// 注意:var a = 1 是函数内部的,编译时也会在函数自己的上下文中处理
值得注意的是,“变量提升”并不代表变量和函数的声明会物理移动到代码最前面。源代码的位置从未改变,只是引擎在编译阶段将它们提前放入内存中。
四、模拟变量提升后的代码
为了更直观地理解,我们可以手动模拟“提升”后的效果:
javascript
// 3.js - 模拟提升后的样子
var myname = undefined;
function showName() {
console.log('showName被执行了');
}
showName();
console.log(myname);
myname = '极客时间';
执行阶段按顺序执行可执行代码,因此 myname 第一次打印时是 undefined,之后才被赋值为 '极客时间'。
五、函数声明与函数表达式的区别
再看一个例子:
javascript
// 2.html
var myName = undefined;
myName = '极客时间';
// 完整的函数声明(没有涉及赋值操作)
function foo() {
console.log('foo');
}
// 函数表达式(涉及赋值操作)
var bar = function() {
console.log('bar');
}
- 函数声明(
function foo() {}):在编译阶段完整提升,函数对象直接可用。 - 函数表达式(
var bar = function() {}):只提升变量bar(值为undefined),赋值操作发生在执行阶段。因此在赋值之前调用bar()会报错。
六、执行上下文与调用栈
执行上下文是 JavaScript 执行一段代码时的运行环境。调用一个函数,就会进入该函数的执行上下文。多个上下文通过调用栈进行管理。
(注:调用栈的详细内容本文不作展开,但它是理解 JavaScript 运行机制的重要概念。)
七、暂时性死区(TDZ):let 与 const 的不同
var 存在变量提升(初始化为 undefined),但 let 和 const 的行为不同。
javascript
// 6.js
console.log(myname); // ReferenceError: Cannot access 'myname' before initialization
let myname = '极客时间';
let和const声明的变量也会被提升,但不会和var“同流合污”。- 它们的内存空间被分配在词法环境中,而不是变量环境。
- 在代码执行阶段,如果在一个变量声明之前访问它,就会报错——这个区域被称为暂时性死区(Temporal Dead Zone, TDZ) 。
暂时性死区到底是什么?
一句话解释:变量已经被 JavaScript 引擎登记在册(在词法环境中声明了),但还没有执行到赋值的那一行,所以你不能使用它。
javascript
// 编译阶段:引擎知道有一个 let myname
// 执行阶段:
console.log(myname); // 此时 myname 已存在词法环境中,但未初始化 → TDZ 报错
let myname = '极客时间';
对比 var:
javascript
// 编译阶段:变量环境中创建 myname = undefined
console.log(myname); // undefined(变量环境中已有值)
var myname = '极客时间';
八、总结
- 变量提升的本质:编译阶段,引擎将变量声明(
var)和函数声明放入内存。var变量默认值为undefined,函数声明直接指向函数对象。 - 执行阶段:按顺序执行可执行代码,对变量进行真正的赋值操作。
- 函数声明 vs 函数表达式:函数声明整体提升,函数表达式只提升变量名。
let/const与暂时性死区:它们也会被提升,但被安排在词法环境中。在声明之前访问会触发 TDZ 错误。- 理解编译与执行:JavaScript 并非纯粹的顺序解释执行,而是“编译 → 执行”交替进行(全局编译一次,每次函数调用编译一次)。
掌握这些原理,不仅能帮你写出更可靠的代码,也能让你在面试和调试中游刃有余。希望这篇文章能帮助你彻底理解 JavaScript 的变量提升、执行上下文和暂时性死区。