JavaScript 执行机制深度解析:从变量提升到词法环境

69 阅读5分钟

前言

lQLPKHfb1wmakBvNAo7NBHaw2WBp0Xeew0kJAa7ewdsuAA_1142_654.png

lQLPJx9tPQkn4xvNAofNBHawrCnzeqtQoLcJAa7itex0AA_1142_647.png JavaScript 的执行机制是理解其行为的核心。我们常看到 var 变量“提前声明”、let/const 报错“无法访问”,这些现象背后,是 V8 引擎对 执行上下文(Execution Context)变量环境(Variable Environment)词法环境(Lexical Environment) 的精妙设计。

本文将结合你提供的代码示例与图示,彻底拆解:

  • 为什么 var 会“提升”而 let/const 不行?
  • 什么是“暂时性死区”(TDZ)?
  • 如何用“一国两制”统一 var 与 let/const
  • ES6 是如何通过词法环境实现块级作用域的?

无论你是准备大厂面试,还是想深入理解 JS 运行原理,这篇文章都将为你打开一扇通往底层世界的大门。


一、JavaScript 执行机制概览

1. V8 引擎与执行流程

JavaScript 由浏览器中的 V8 引擎 解析执行,其过程分为两个阶段:

阶段内容
编译阶段解析代码、构建抽象语法树(AST)、生成字节码
执行阶段创建执行上下文、调用栈管理、变量查找

关键点:JS 是“先编译后执行”的语言,不是纯解释型。

2. 调用栈(Call Stack)

  • 每次函数调用,都会创建一个 执行上下文 并压入调用栈
  • 函数执行完毕,上下文出栈并回收资源
  • 实现了函数嵌套调用的顺序控制
js
编辑
function a() {
    b();
}
function b() {
    console.log('hello');
}
a(); // 调用栈:[全局, a(), b()]

二、执行上下文:变量的“家”

每个函数或全局代码块在执行时,都会创建一个 执行上下文(Execution Context) ,它包含三个核心部分:

组件作用
变量环境(Variable Environment)存储 var 声明的变量,支持“变量提升”
词法环境(Lexical Environment)存储 letconst、函数声明等,支持“块级作用域”
this 绑定确定当前作用域的 this 指向

📌 本质:执行上下文是变量的“容器”,决定了变量的可见性与生命周期。


三、变量提升(Hoisting):var 的“特权”

1. 什么是变量提升?

var 声明的变量在编译阶段被“提升”到作用域顶部,并初始化为 undefined

js
编辑
console.log(a); // undefined
var a = 1;

实际执行流程

js
编辑
var a;           // 提升并初始化为 undefined
console.log(a);  // 输出 undefined
a = 1;           // 赋值

2. var 的问题:不科学的设计缺陷

  • 容易覆盖:同一作用域内多次声明不会报错
  • 无块级作用域iffor 中的 var 仍影响外部
  • 生命周期混乱:本该销毁的变量未被清理
js
编辑
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 → 外部可访问!

💡 历史原因
JS 初期为快速推出而设计,受浏览器竞争驱动,追求简单而非严谨。当时目标只是“给网页加动态效果”,复杂特性如类、继承、作用域都未充分考虑。


四、ES6 的救赎:let / const 与“一国两制”

1. “一国两制”:两种环境并存

类型存放位置行为
var变量环境提升 + 初始化为 undefined
let / const词法环境提升但不初始化 → 暂时性死区(TDZ)

设计哲学

  • var 保持兼容,延续旧有行为
  • let/const 引入新规则,提升代码安全性

五、暂时性死区(Temporal Dead Zone, TDZ)

1. 什么是 TDZ?

let / const 声明之前访问变量,会抛出 ReferenceError,因为变量虽被“声明”,但尚未“初始化”。

js
编辑
let name = '刘锦苗';
{
    console.log(name);      // ❌ ReferenceError!进入 TDZ
    let name = "大厂的苗子";
}

2. 为什么会出现 TDZ?

  • let 声明被提升到词法环境,但初始值为  “未定义”状态
  • 在赋值前,任何读取操作都视为“非法访问”
  • 目的是防止意外使用未初始化的变量

🔍 注意console.log(name) 访问的是块内的 name,不是外层的,因为存在“词法遮蔽”(Lexical Shadowing)。


六、词法环境:块级作用域的实现机制

1. 词法环境的结构:小型栈

词法环境内部维护了一个栈式结构,每进入一个块级作用域,就压入一个新的“环境记录”。

js
编辑
function foo() {
    var a = 1;
    var b = 2;
    {
        let b = 3; // 新环境记录
        var c = 4;
        let d = 5;
        console.log(b); // 3(栈顶)
    }
    console.log(b); // 2(回到上一层)
}

2. 图解:foo 函数的执行上下文

👉 执行前(编译阶段)

plaintext
编辑
[全局]
├── 变量环境
│   ├── a = undefined
│   └── c = undefined
└── 词法环境
    └── b = undefined

👉 执行中(进入块)

plaintext
编辑
[foo 函数的执行上下文]
├── 变量环境
│   ├── a = 1
│   └── c = undefined
└── 词法环境
    ├── [块级环境1] 
    │   ├── b = undefined
    │   └── d = undefined
    └── [块级环境2] 
        └── b = 2

图示说明

  • 红色区域:变量环境(存放 var
  • 橙色区域:词法环境(存放 let/const
  • 栈结构:块级作用域执行时,优先查找栈顶环境

七、经典案例分析

示例1:var vs let 的差异

js
编辑
// var 版本
var name = "刘锦苗";
function showName() {
    console.log(name); // undefined
    if (true) {
        var name = "大厂的苗子";
    }
    console.log(name); // "大厂的苗子"
}
showName();

// let 版本
let name = "刘锦苗";
function showName() {
    console.log(name); // "刘锦苗"
    if (false) {
        let name = "大厂的苗子"; // 不执行,不影响
    }
    console.log(name); // "刘锦苗"
}
showName();

示例2:for 循环中的 let 与 var

js
编辑
// var:循环变量共享
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
} // 输出 3,3,3

// let:每次迭代创建新绑定
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
} // 输出 0,1,2

💡 原因let 在每次循环中创建新的块级作用域,i 是独立的。


八、大厂面试题精选

  1. let 会提升吗?
    → 会提升声明,但不会提升初始化,进入 TDZ。
  2. 为什么 let 在声明前访问会报错?
    → 因为处于“暂时性死区”,变量未初始化。
  3. ES6 如何实现块级作用域?
    → 通过词法环境的栈结构,在每个块内创建独立的环境记录。
  4. varlet 的存储位置有何不同?
    var 在变量环境,let 在词法环境。

结语

JavaScript 的执行机制看似复杂,实则是一套精心设计的“双轨制”系统:

  • var 保留兼容性,走“变量环境”老路
  • let/const 引入现代规范,走“词法环境”新道

这正是 ES6 的伟大之处——既尊重历史,又拥抱未来。理解这一机制,不仅能避免常见陷阱,更能写出更安全、更清晰的代码。

记住
“变量提升”不是 bug,而是设计;
“暂时性死区”不是限制,而是保护。
真正的高手,懂得在规则中寻找自由。