一、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 的预编译过程!如果你有任何疑问或想法,欢迎随时交流