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。
执行流程:
a = 3(来自实参);console.log(a)→3;a = 2(赋值覆盖);b = a→b = 2;- 最终输出
2。
📌 注意:若将
var a = 2替换为function a() {},则函数声明会覆盖形参(函数提升优先级更高)。
五、var 与 let/const 的本质区别
| 特性 | var | let / 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('函数表达式不会提升');
};
func是let声明的变量,绑定的是一个箭头函数;- 编译阶段:
func被放入 TDZ; - 执行阶段:在赋值前调用 →
func为undefined,不是函数。
✅ 只有 函数声明(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)工作流程:
- 全局代码 → 创建 全局执行上下文,压入栈底;
- 遇到函数调用 → 创建 函数执行上下文,压入栈顶;
- 函数执行完毕 → 上下文出栈,内存释放;
- 栈空 → 程序结束。
执行上下文结构:
{
VariableEnvironment: { /* var, function */ },
LexicalEnvironment: { /* let, const, TDZ */ },
ThisBinding: { /* this 指向 */ },
Code: { /* 待执行的代码 */ }
}
九、总结:开发者应牢记的要点
- JavaScript 先编译后执行,提升是编译阶段的行为;
- 函数声明提升 > var 提升 > let/const(仅声明) ;
- let/const 有暂时性死区(TDZ) ,声明前不可访问;
- var 允许重复声明,let/const 不允许;
- 函数表达式不会提升,只有函数声明会;
- 基本类型值拷贝,引用类型地址拷贝;
- 调用栈管理执行上下文,函数执行完即销毁。
📚 延伸建议:
- 使用
let/const替代var,避免提升陷阱;- 避免在声明前使用变量(即使
var允许);- 理解闭包、this、事件循环需建立在此机制之上。