前言
在了解预编译之前,我们应该先了解一下v8引擎是怎样工作的,首先它需要将代码分成一个个词法单元,如 'var' ,'a',等,再进行语法分析,将词法单元组成 AST(抽象语法树),最后根据AST生成代码执行。那么预编译就是发生在代码执行之前,让我们来看一个例子。
js
console.log(a); //输出 undefined
var a = 1
按照大家所理解的代码执行步骤,应该是从上到下依次执行,那么它应该报错,但是最后显示的是undefined,这是为什么呢?这是因为在代码执行之前,它先进行了编译这个过程,那编译是怎么做呢?
js
var a //定义变量 a 将它提升到当前作用域的顶部
console.log(a); // 得到undefined 找不到a的值
a = 1
将变量提升到当前作用域的顶部,这个过程我们称之为声明提升 ,此处为全局作用域,所以得到输出结果为undefined,那为什么出现声明提升呢?我们就要具体了解一下预编译过程是怎么样的。
预编译
预编译发生在代码执行之前,它分为两类 函数体内的预编译和全局预编译,函数的预编译是在函数被调用时才触发,而全局预编译是在代码加载时就触发了,那它们有以下几个步骤。
1.函数体内的预编译
- 创建一个执行上下文 AO:{}
- 找形参和变量声明,将形参和变量名作为属性名,添加到AO中,值为 undefined
- 将形参和实参统一
- 在函数体内找函数声明,将函数名作为AO 中的属性名,函数体作为属性值
让我们来看一个例子,根据步骤依次梳理
js
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
var b = function() {}
console.log(b);
function c() {}
var c = a
console.log(c);
}
fn(1) //调用函数
首先 创造一个执行上下文 AO:{}
其次 找到形成和变量声明,将形参和变量名作为属性名,添加到AO中,也就是说找到 形参a 变量a b c 添加到AO中, 值为 undefined,即有
js
AO = {
a:undefined //出现了形参a 又出现变量a 则进行覆盖 对象中只有一个key
b:undefined
c:undefined
}
然后将 将形参和实参统一那么 a此时 值为1,即有
js
AO = {
a:undefined →1
b:undefined
c:undefined
}
最后找到在函数体内找函数声明,将函数名作为AO 中的属性名,函数体作为属性值即有
js
AO = {
a:undefined →1→function a(){}
b:undefined
c:undefined→function c(){}
}
已经完成预编译过程,那就开始执行代码,所以有
js
function fn(a) {
console.log(a); // 输出结果 function a(){}
var a = 123 // 把123赋值给a
console.log(a); / 输出 a值为123
function a() {}
var b = function() {} 把function(){} 赋给b
console.log(b); // 输出为 function(){}
function c() {}
var c = a //把a赋给c c为123
console.log(c); // 输出123
// AO = {
// a:undefined →1 →function a(){} →123
// b:undefined → function b(){}
// c:undefined →function c(){} →123
}
fn(1) //调用函数
结果也正如我们所判断的这样

2.全局预编译
- 创建全局执行上下文 GO:{}
- 找全局变量声明,将全局变量名作为属性名,添加到GO中,值为 undefined
- 找全局函数声明,将函数名作为GO 的属性名,函数体作为属性值
依旧来看一个例子,根据步骤依次梳理
js
var a
var b = 2
function a(){
console.log(a);
var c = 3
var a = b
function c(){}
console.log(c);
}
a()
console.log(a);
首先,创建全局上下文GO:{}
其次,找全局变量声明,将全局变量声明作为属性名,添加到GO中,也就说将变量 a,b添加到GO中,值为undefined,即有
js
GO = {
a: undefined,
b: undefined
}
最后,找全局函数声明,将函数名作为GO 的属性名,函数体作为属性值,即有
js
GO = {
a: undefined →function a(){xxx},
b: undefined
}
全局编译已经完成,开始执行代码,所以有
js
//GO = {
// a: undefined →function a(){xxx},
// b: undefined
//}
var a
var b = 2 // 把 2 赋给 b
function a(){
console.log(a);
var c = 3
var a = b
function c(){}
console.log(c);
}
a() //调用函数 function a(){}
console.log(a); //输出 function a(){}
所以 11 行处代码 输出为function a(){},而代码 有a(),说明调用了函数function a(){xxx},那我们又需要对函数进行预编译,与上文步骤相同,所以我们可以得到
js
//GO = {
// a: undefined→ function a(){xxx},
// b: undefined
//}
var a
var b = 2
function a(){
// AO = {
// c: undefined→ function c(){}→ 3
// a: undefined→ b 即 2
//}
console.log(a); //输出 undefined
var c = 3 // 把3赋给c
var a = b // 把b的值赋a 函数中没有,去外层找,得到b值为2 即a值为 2
function c(){}
console.log(c); //输出 3
}
a()
console.log(a); //输出 function a(){}
全局预编译GO与函数体内预编译AO的关系
-
执行顺序:先全局预编译 → 再函数调用时执行函数预编译
-
作用域层级:全局是最外层,代码执行时开始编译→函数预编译依附全局,形成层级作用域,只有当调用函数时才会开始编译
-
优先级:函数内部变量 > 全局变量,内部优先访问自身预编译内容,找不到再去全局里面找,而全局找不到,不会去函数内部找 即(小可以去大找,大不会去小找)
-
互不冲突:同名变量互不覆盖,只遵循作用域就近原则
(如有补充,请大佬指点)
