JavaScript 执行机制深度解析:编译、执行上下文、变量提升、TDZ 与内存模型

178 阅读5分钟

彻底读懂 JavaScript 执行机制:编译、执行上下文、变量提升、TDZ 与内存模型全解析

很多初学者在学习 JavaScript 时都会遇到这些常见问题:

  • 为什么变量打印出来是 undefined
  • 为什么访问 letconst 会抛出 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 以“函数”为单位执行代码:

  1. 全局执行上下文压入栈底
  2. 调用函数 → 创建函数执行上下文 → 入栈
  3. 函数执行完 → 出栈并销毁
  4. 所有代码执行完 → 全局上下文退出

JS 执行机制的“栈结构”保证了函数调用顺序和作用域的正确管理。


四、核心示例解析

示例 1:函数声明提升 + TDZ

showName();
console.log(myName);
console.log(hero);

var myName = 'ouma_syu';
let hero = '钢铁侠';

function showName() {
    console.log(myName);
    console.log('函数showName被执行');
}

编译阶段:

  • 变量环境:
名称
showName函数体
myNameundefined
  • 词法环境:
名称状态
heroTDZ(未初始化)

执行阶段输出:

undefined
函数showName被执行
undefined
ReferenceError: Cannot access 'hero' before initialization

要点:

  • 函数声明提升优先级最高,可提前调用
  • var 提升为 undefined
  • let/const 进入 TDZ,访问报错

示例 2:变量提升本质

var myName;
function showName() {
    console.log('函数showName被');
}
showName();
console.log(myName);
myName = 'ouma_syu';

提升后的逻辑等价于:

var myName = undefined;
function showName() {...}

JS 提前知道当前作用域有哪些变量与函数,保证“先调用后定义”可行。


示例 3:形参与 var 冲突

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 差异

console.log(a); // undefined
console.log(b); // ReferenceError

var a = 1;
console.log(a); // 1

let b = 3;
console.log(b); // 3
  • var 提升并初始化为 undefined
  • let 提升但未初始化,访问报错

示例 5:函数表达式不提升

fn();  // ReferenceError

let fn = () => {
    console.log('函数表达式不会提升');
}
  • let 声明在 TDZ 中无法访问
  • 即使 var fn,调用也会报 TypeError
  • 函数表达式本质是变量,不像函数声明完全提升

示例 6:基本类型 vs 引用类型

基本类型(值存储,栈内存)

let str = 'hello';
let str2 = str;

str2 = '你好';
console.log(str, str2); // hello 你好

引用类型(地址存储,堆内存)

let obj = { name:'ouma_syu', age:18 };
let obj2 = obj;

obj.age++;
console.log(obj, obj2); // {age:19}, {age:19}

基本类型复制的是值,引用类型复制的是地址。修改引用类型时,所有指向该地址的变量都会受到影响。


五、JS 执行机制最终总结

  1. 先编译,后执行
  2. 执行上下文 = 变量环境 + 词法环境 + this
  3. 提升规则是理解 JS 行为的基础
类型是否提升初始化状态可提前访问访问行为作用域注意事项
函数声明完全提升函数体已准备好可提前调用正常调用当前上下文优先级最高,覆盖同名 var
var声明提升undefined可访问undefined,赋值后生效函数或全局可重复声明,不会报错
let注册提升未初始化(TDZ)不可访问ReferenceError块级作用域声明后使用;进入 TDZ 直到执行
const注册提升未初始化(TDZ)不可访问ReferenceError块级作用域声明时必须初始化;不可重复赋值
  1. 调用栈保证以函数为单位执行
  2. 基本类型存值,引用类型存地址

掌握这些底层机制后,JS 中的大部分“奇怪行为”都能预测和理解。