JavaScript 执行机制深度解析:V8 引擎视角下的变量提升、作用域与调用栈

78 阅读4分钟

JavaScript 执行机制深度解析:V8 引擎视角下的变量提升、作用域与调用栈

本文基于 V8 引擎执行模型,系统梳理 JavaScript 的编译与执行机制,涵盖 var/let/const 差异、函数提升、执行上下文、调用栈等核心概念,帮助开发者真正理解“代码为何这样运行”。


一、JavaScript 是如何被执行的?

JavaScript 虽然是解释型语言,但在现代引擎(如 Chrome 的 V8)中,实际采用的是 “即时编译”(JIT) 模式:

  • 先编译,再执行
  • 编译发生在执行前的“一瞬间”;
  • 编译阶段会进行语法检查变量/函数提升
  • 执行依赖于 调用栈(Call Stack) 管理执行上下文。

二、执行的两个阶段

1. 编译阶段(Compilation Phase)

  • 创建 执行上下文(Execution Context)

  • 扫描代码,处理:

    • var 声明 → 提升至 变量环境(Variable Environment) ,初始值为 undefined
    • let/const 声明 → 放入 词法环境(Lexical Environment) ,处于 暂时性死区(TDZ)
    • 函数声明 → 完整提升(函数名 + 函数体),优先级高于变量;
  • 不执行赋值或逻辑代码。

2. 执行阶段(Execution Phase)

  • 按代码顺序执行;
  • 对变量进行赋值;
  • 调用函数时,创建新的执行上下文并压入调用栈;
  • 函数执行完毕后,上下文出栈并被垃圾回收。

三、变量提升 vs 函数提升

示例 1:混合声明

showName();        // ✅ 输出: undefined + "函数showName被执行"
console.log(myName); // undefined
console.log(hero);   // ❌ ReferenceError: Cannot access 'hero' before initialization

var myName = 'inx177';
let hero = 'batman';

function showName() {
  console.log(myName);
  console.log('函数showName被执行');
}
V8 编译后的逻辑等价代码:
// 编译阶段处理
var myName;           // → undefined(变量环境)
let hero;             // → TDZ(词法环境,不可访问)
function showName() { // → 完整函数提升(优先级最高)
  console.log(myName);
  console.log('函数showName被执行');
}

// 执行阶段
showName();           // myName 仍为 undefined
console.log(myName);  // undefined
console.log(hero);    // 报错!let 在 TDZ 中
myName = 'inx177';
hero = 'batman';

关键结论

  • function 声明提升 > var 提升 > let/const(仅声明,不初始化);
  • let/const 存在 暂时性死区(Temporal Dead Zone, TDZ) ,在声明前访问会抛错。

四、函数参数与变量声明的优先级

示例 2:形参与 var 同名

var a = 1;
function fn(a) {
  console.log(a);   // 3
  var a = 2;
  var b = a;
  console.log(a);   // 2
}
fn(3);
console.log(a);     // 1
编译阶段(函数 fn 的执行上下文):
  • 形参 a 接收实参 3
  • 遇到 var a → 由于 a 已存在(形参),忽略重复声明
  • var b → 提升为 b = undefined
执行流程:
  1. a = 3(来自实参);
  2. console.log(a)3
  3. a = 2(赋值覆盖);
  4. b = ab = 2
  5. 最终输出 2

📌 注意:若将 var a = 2 替换为 function a() {},则函数声明会覆盖形参(函数提升优先级更高)。


五、varlet/const 的本质区别

特性varlet / const
提升方式提升至变量环境,值为 undefined提升至词法环境,但处于 TDZ
重复声明允许(静默忽略)不允许(SyntaxError)
作用域函数作用域块级作用域
全局对象绑定是(window.a

示例 3:重复声明对比

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

var a = 1;
var a = 2;      // ✅ 合法,覆盖

let b = 3;
// let b = 4;   // ❌ SyntaxError: Identifier 'b' has already been declared

⚠️ 即使在 严格模式('use strict') 下,var 仍可重复声明,而 let/const 始终禁止。


六、函数表达式不会提升!

func(); // ❌ TypeError: func is not a function
let func = () => {
  console.log('函数表达式不会提升');
};
  • funclet 声明的变量,绑定的是一个箭头函数;
  • 编译阶段:func 被放入 TDZ;
  • 执行阶段:在赋值前调用 → funcundefined,不是函数。

✅ 只有 函数声明(Function Declaration) 会被完整提升。


七、数据类型与内存模型

1. 基本类型(栈内存)

let str = 'hello';
let str2 = str; // 值拷贝
str2 = 'nihao';
console.log(str, str2); // 'hello' 'nihao'
  • 存储在 栈(Stack)
  • 赋值时复制值本身。

2. 引用类型(堆内存)

let obj = { name: 'inx177', age: 18 };
let obj2 = obj; // 引用拷贝(共享地址)
obj2.age++;
console.log(obj, obj2); // { age: 19 } { age: 19 }
  • 对象存储在 堆(Heap)
  • 变量保存的是 内存地址
  • 赋值是复制地址,多个变量指向同一对象。

💡 理解这一点,才能避免“意外修改原始对象”的 bug。


八、V8 执行机制全景图

调用栈(Call Stack)工作流程:

  1. 全局代码 → 创建 全局执行上下文,压入栈底;
  2. 遇到函数调用 → 创建 函数执行上下文,压入栈顶;
  3. 函数执行完毕 → 上下文出栈,内存释放;
  4. 栈空 → 程序结束。

执行上下文结构:

{
  VariableEnvironment: { /* var, function */ },
  LexicalEnvironment:  { /* let, const, TDZ */ },
  ThisBinding:         { /* this 指向 */ },
  Code:                { /* 待执行的代码 */ }
}

九、总结:开发者应牢记的要点

  1. JavaScript 先编译后执行,提升是编译阶段的行为;
  2. 函数声明提升 > var 提升 > let/const(仅声明)
  3. let/const 有暂时性死区(TDZ) ,声明前不可访问;
  4. var 允许重复声明,let/const 不允许
  5. 函数表达式不会提升,只有函数声明会;
  6. 基本类型值拷贝,引用类型地址拷贝
  7. 调用栈管理执行上下文,函数执行完即销毁

📚 延伸建议

  • 使用 let/const 替代 var,避免提升陷阱;
  • 避免在声明前使用变量(即使 var 允许);
  • 理解闭包、this、事件循环需建立在此机制之上。