一、JavaScript执行流程全景图
JavaScript代码的执行流程可以概括为以下关键步骤:
- 读取代码:V8引擎加载JS代码
- 预编译阶段:创建执行上下文,进行变量/函数声明提升
- 执行阶段:逐行执行代码,进行赋值和函数调用

二、函数预编译四步曲
当函数被调用时,V8引擎会创建函数执行上下文,其预编译过程包含四个关键步骤:
-
创建函数执行上下文对象(AO)
javascriptfunction example(a, b) { var c = 10; function d() {} var e = function() {}; } example(1,2) // 预编译开始:创建函数的执行上下文对象 example = {} -
寻找形参和变量声明
-
将形参和变量名作为执行上下文对象的属性名
-
值为undefined
javascriptexample = { a: undefined, // 形参 b: undefined, // 形参 c: undefined, // 变量声明 e: undefined // 变量声明 } -
-
实参与形参值统一
javascriptexample(1, 2); // 调用时实参赋值 example = { a: 1, // 形参绑定实参值 b: 2, // 形参绑定实参值 c: undefined, e: undefined } -
找函数声明
-
函数声明会覆盖同名的变量
-
在函数体里找函数声明,函数名作为上下文对象的属性名
-
值为函数体
javascriptAO = { a: 1, b: 2, c: undefined, d: function d() {}, // 函数声明直接赋值 e: undefined } -
三、全局预编译三阶段
全局作用域的预编译过程略有不同:
-
创建全局上下文对象(GO)
csharp// 全局代码 var a = 'test'; function fn() {} // 创建GO对象 GO = {} -
找变量声明
- 变量名作为 全局上下文对象的属性名
- 值为undefined
cssGO = { a: undefined // 变量声明提升 } -
找函数声明
- 函数名作为上下文对象属性名
- 值为函数体
javascriptGO = { a: undefined, a: function fn() {} // 函数声明提升 }
四、预编译实战解析
看了那么多,来上手试试,下面代码的结果是什么?
1
css
var a=1
function fn(){
var a=2
function a(){}
var b=a
console.log(a);
}
fn()
预编译过程:

第一步,先创建全局上下文执行对象 放入调用栈 在GO中 。第二步查找变量声明 a 值设为undefined。第三步, 然后查找函数声明,函数名fn作为属性名,值为函数体function(){},最后进行代码执行,a=1 并且进入函数体中创建函数的执行上下文对象,查找形参和变量名 ,a值设为undefined ,b值设为undefined,,因为没有形参,所以不需要实参形参值统一 直接在函数体fn中查找函数声明 ,这里需要注意的是变量名和函数名一样的就直接覆盖,不会再创建一个变量,因为是在对象中key是唯一的 ,找到a的函数声明,a值改为函数体,预编译结束,执行代码 执行第3行a值改为2 第4行函数声明不调用不执行,第5行执行b值改为2,第6行输出a,a值为2
结果:

2
js
function fn(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
var b = function () {};
console.log(b);
function d() {}
var d = a;
console.log(d);
}
fn(1);

第一步,先创建全局上下文执行对象 这里没有变量声明,直接代码执行。第二步,在函数体中创建函数的执行上下文对象,查找形参和变量名 ,形参a值设为undefined,变量a重复 ,b值设为undefined,d值设为undefined。第三步,然后形参和实参值统一 a=1。第四步,在函数体fn中查找函数声明 ,这里需要注意的是var b=fucntion(){}不是函数声明哦,聪明的小伙伴肯定发现了,这是函数表达式,相当赋值要等到执行阶段执行 ,找到d的函数声明,d值改为函数体,预编译结束,执行代码 执行第2行 打印空函数体 执行第3行a值改为123 第4行打印a的结果为123,第5行跳过,第6行输出b值赋为函数体,第7行打印b为空的函数体,第8行跳过,第9行d值改为123,第10行打印d值为123 结果:

3
js
function fnn(a, b) {
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b() {}
console.log(b);
}
fn(1);
第一步先创建全局上下文执行对象 这里没有变量声明,直接代码执行。第二步在函数体中创建函数的执行上下文对象,查找形参和变量名 ,形参a值设为undefined变量a重复 ,形参b值设为undefined,c值设为undefined,然后形参和实参值统一 a=1,b值没有。第三步在函数体fn中查找函数声明 ,这里需要注意的是var b=fucntion(){}不是函数声明哦,聪明的小伙伴肯定发现了,这是函数表达式,相当赋值要等到执行阶段执行 ,找到b的函数声明,b值改为函数体,预编译结束,执行代码 执行第2行 ,打印结果1, 执行第3行c值改为0 第4行跳过,第5行a值设为3,第6行输出b值赋2,第7行打印b为2,第8行跳过,第9行打印b值为2 结果:

五、关键概念对比表
| 特性 | 变量声明 | 函数声明 |
|---|---|---|
| 提升机制 | 仅声明提升(值为undefined) | 整个函数体提升 |
| 执行上下文创建时机 | 预编译阶段 | 预编译阶段 |
| 重复声明处理 | 后续声明被忽略 | 覆盖同名变量 |
| 块级作用域影响 | 受let/const约束 | 不受块级作用域影响 |
六、ES6带来的改变
-
暂时性死区(TDZ)
javascript
iniconsole.log(a); // ReferenceError let a = 10; -
块级作用域
javascript
javascript{ let b = 1; function fn() {} } console.log(b); // ReferenceError console.log(fn); // 函数可访问(非严格模式)
七、总结
JavaScript的预编译机制是其执行模型的核心:
- 全局预编译:创建GO → 变量声明 → 函数声明
- 函数预编译:创建AO → 形参/变量 → 实参绑定 → 函数声明
- ES6引入的let/const通过块级作用域和TDZ解决了变量提升问题
- 理解执行上下文生命周期是掌握JavaScript异步编程、闭包等高级特性的基础
希望本文能帮助你更好地理解 JavaScript 的预编译过程!如果你有任何疑问或想法,欢迎随时交流