彻底读懂 JavaScript 执行机制:编译、执行上下文、变量提升、TDZ 与内存模型全解析
很多初学者在学习 JavaScript 时都会遇到这些常见问题:
- 为什么变量打印出来是
undefined? - 为什么访问
let或const会抛出ReferenceError? - 为什么函数声明可以提前调用,而函数表达式不行?
- 为什么两个对象会"联动修改"?
- 为什么形参与
var同名时行为异常?
这些看似独立的问题,其实都源于同一个核心原理:
JavaScript 的执行机制 ------ "先编译,后执行" 。
理解这一点,你就真正掌握了 JS 的底层运行逻辑。
一、JS 运行机制:编译阶段与执行阶段
JavaScript 属于"解释型 + 编译优化"的语言,执行流程分为两个阶段:
1. 编译阶段(Creation Phase)
在编译阶段,V8 会进行以下操作:
- 创建执行上下文(Execution Context)
- 处理
var变量提升 - 处理函数声明提升
- 注册
let/const(但不初始化,进入暂时性死区 TDZ)
编译阶段就是 JS 在执行前"扫一遍",把变量、函数和作用域信息先登记好。
2. 执行阶段(Execution Phase)
逐行执行代码,变量赋值、函数调用在此阶段完成。
- 这解释了为什么
var变量会被提升为undefined - 以及为什么访问
let/const会触发 TDZ
二、执行上下文(Execution Context)
每段代码的执行都有一个"容器",即执行上下文对象(Execution Context),它包含三个主要部分:
1. 变量环境(Variable Environment)
- 存放
var声明(初始化为undefined) - 存放函数声明(直接绑定函数体)
2. 词法环境(Lexical Environment)
- 存放
let/const声明(仅注册,不初始化) - 存放块级作用域变量
- TDZ(暂时性死区)就发生在这里
3. this 绑定
- 决定
this指向 - 全局上下文、函数上下文、箭头函数各自规则不同
理解执行上下文,就能解释变量提升、函数调用顺序以及 TDZ 的行为。
三、调用栈(Call Stack)
JS 以"函数"为单位执行代码:
- 全局执行上下文压入栈底
- 调用函数 → 创建函数执行上下文 → 入栈
- 函数执行完 → 出栈并销毁
- 所有代码执行完 → 全局上下文退出
JS 执行机制的"栈结构"保证了函数调用顺序和作用域的正确管理。
四、核心示例解析
示例 1:函数声明提升 + TDZ
ini
showName();
console.log(myName);
console.log(hero);
var myName = 'ouma_syu';
let hero = '钢铁侠';
function showName() {
console.log(myName);
console.log('函数showName被执行');
}
编译阶段:
- 变量环境:
| 名称 | 值 |
|---|---|
| showName | 函数体 |
| myName | undefined |
- 词法环境:
| 名称 | 状态 |
|---|---|
| hero | TDZ(未初始化) |
执行阶段输出:
javascript
undefined
函数showName被执行
undefined
ReferenceError: Cannot access 'hero' before initialization
要点:
- 函数声明提升优先级最高,可提前调用
var提升为undefinedlet/const进入 TDZ,访问报错
示例 2:变量提升本质
ini
var myName;
function showName() {
console.log('函数showName被');
}
showName();
console.log(myName);
myName = 'ouma_syu';
提升后的逻辑等价于:
javascript
var myName = undefined;
function showName() {...}
JS 提前知道当前作用域有哪些变量与函数,保证"先调用后定义"可行。
示例 3:形参与 var 冲突
ini
var a = 1;
function fn(a) {
console.log(a);
var a = 2;
var b = a;
console.log(a);
}
fn(3);
fn(3);
console.log(a);
执行结果:
3
2
3
2
1
解析:
- 形参优先于
var声明 - 函数内部变量与全局变量独立
var重复声明会被忽略
示例 4:var 与 let 差异
ini
console.log(a); // undefined
console.log(b); // ReferenceError
var a = 1;
console.log(a); // 1
let b = 3;
console.log(b); // 3
var提升并初始化为undefinedlet提升但未初始化,访问报错
示例 5:函数表达式不提升
php
fn(); // ReferenceError
let fn = () => {
console.log('函数表达式不会提升');
}
let声明在 TDZ 中无法访问- 即使
var fn,调用也会报TypeError - 函数表达式本质是变量,不像函数声明完全提升
示例 6:基本类型 vs 引用类型
基本类型(值存储,栈内存)
ini
let str = 'hello';
let str2 = str;
str2 = '你好';
console.log(str, str2); // hello 你好
引用类型(地址存储,堆内存)
ini
let obj = { name:'ouma_syu', age:18 };
let obj2 = obj;
obj.age++;
console.log(obj, obj2); // {age:19}, {age:19}
基本类型复制的是值,引用类型复制的是地址。修改引用类型时,所有指向该地址的变量都会受到影响。
五、JS 执行机制最终总结
- 先编译,后执行
- 执行上下文 = 变量环境 + 词法环境 + this
- 提升规则是理解 JS 行为的基础
| 类型 | 是否提升 | 初始化状态 | 可提前访问 | 访问行为 | 作用域 | 注意事项 |
|---|---|---|---|---|---|---|
| 函数声明 | 完全提升 | 函数体已准备好 | 可提前调用 | 正常调用 | 当前上下文 | 优先级最高,覆盖同名 var |
| var | 声明提升 | undefined | 可访问 | undefined,赋值后生效 | 函数或全局 | 可重复声明,不会报错 |
| let | 注册提升 | 未初始化(TDZ) | 不可访问 | ReferenceError | 块级作用域 | 声明后使用;进入 TDZ 直到执行 |
| const | 注册提升 | 未初始化(TDZ) | 不可访问 | ReferenceError | 块级作用域 | 声明时必须初始化;不可重复赋值 |
- 调用栈保证以函数为单位执行
- 基本类型存值,引用类型存地址
掌握这些底层机制后,JS 中的大部分"奇怪行为"都能预测和理解。