深入理解 JavaScript 的执行原理:从变量提升到暂时性死区

0 阅读6分钟

深入理解 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 而不是变量的真实值。

如果我们对比传统认知中“代码按顺序执行”的预期,会发现两个“奇怪”的现象:

  1. 在变量定义之前使用它:不会报错,但该变量的值是 undefined,而不是定义时的值。
  2. 在函数定义之前使用它:不会出错,且函数能够正确执行。

另外,如果使用了从未声明的变量,则会报错:

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被执行了');
}

在编译阶段,引擎会为当前作用域准备好变量环境:

  1. 找到所有变量声明var),在环境中创建该变量并初始化为 undefined
  2. 找到所有函数声明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 = '极客时间';

八、总结

  1. 变量提升的本质:编译阶段,引擎将变量声明(var)和函数声明放入内存。var 变量默认值为 undefined,函数声明直接指向函数对象。
  2. 执行阶段:按顺序执行可执行代码,对变量进行真正的赋值操作。
  3. 函数声明 vs 函数表达式:函数声明整体提升,函数表达式只提升变量名。
  4. let / const 与暂时性死区:它们也会被提升,但被安排在词法环境中。在声明之前访问会触发 TDZ 错误。
  5. 理解编译与执行:JavaScript 并非纯粹的顺序解释执行,而是“编译 → 执行”交替进行(全局编译一次,每次函数调用编译一次)。

掌握这些原理,不仅能帮你写出更可靠的代码,也能让你在面试和调试中游刃有余。希望这篇文章能帮助你彻底理解 JavaScript 的变量提升、执行上下文和暂时性死区。