在了解预编译之前先插入一些小知识点
var声明变量的声明提升
console.log(a);
var a = 123
// 执行结果:undefined
执行结果为undefined,说明console.log(a);
执行时,它已经知道有a了,只是找不到值,所以为undefined,如果在执行时它不知道有a的话就会报错。这是因为var声明变量时会存在声明提升,可以理解为代码被写成了这样:
var a
console.log(a);
a = 123
函数声明的整体提升
foo()
function foo(){
var a = 123
console.log(a);
}
// 执行结果:123
按直觉来说,v8执行引擎从上往下执行,在调用foo()时似乎是找不到函数的内容的,但是执行结果却为123,这是因为函数的声明整体提升,函数的声明会提升到其所处的作用域的最顶端,所以foo()调用函数时可以正常运行
好了,在了解完这两个小知识点之后我们便对代码的执行过程有了更深入的认识,但是假设面试官给你敲一些更加复杂的代码时,我们应该如何去梳理一些复杂的代码的执行过程呢?我们便需要了解js的预编译了,了解预编译可以以更底层的角度去了解代码的执行过程
预编译发生在函数执行之前(四部曲)
- 创建AO对象 (Action Object)
- 找形参和变量声明(有效标识符),将变量声明和形参作为AO的属性名,值为undefined
- 将形参和实参值统一
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
这四步究竟是怎样完成函数的预编译呢,我们以下面这段代码为例,一步一步分析
function fn(a){
console.log(a);
var a = 123
console.log(a)
function a(){} //函数声明
console.log(a);
var b = function(){} //函数表达式
console.log(b);
function d(){}
var d = a
console.log(d);
}
fn(1)
第一步:创建AO对象 (Action Object)
AO:{
}
在编译函数之前会为函数创建一个AO对象用AO对象来表达这个函数的作用域
第二步:找形参和变量声明(有效标识符),将变量声明和形参作为AO的属性名,值为undefined
AO:{
a : undefined,
b : undefined,
d : undefined
}
在函数中,我们找到了形参a
,var a = 123
,var b = function(){}
,var d = a
,我们将a,b,d作为AO的属性名,值为undefined。需要注意的是虽然形参a和var a = 123都声明了a,但只有一个a : undefined,是因为对象中不能有两个同样的key,第一个a : undefined会被第二个a : undefined覆盖掉
第三步:将形参和实参值统一
AO:{
a : 1,
b : undefined,
d : undefined
}
fn接收了一个实参1,将1赋值给a便完成了形参和实参值的统一
第四步:在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
AO:{
a : function a(){},
b : undefined,
d : function d(){}
}
在代码中我们找到了function a(){}和function d(){}两个函数声明,所以a的值又被改为function a(){},并且将function d(){}给到d
到此函数的预编译就完成了!执行引擎便开始放心的执行代码了
预编译完成后的执行
function fn(a){
console.log(a); // 打印[Function: a]
var a = 123
console.log(a) // 打印123
function a(){} // 函数声明
console.log(a); // 打印123
var b = function(){} // 函数表达式
console.log(b); // 打印[Function: b]
function d(){}
var d = a
console.log(d); // 打印123
}
fn(1)
特别注意在第一个console.log(a);
执行时会去到AO对象里面找里面a的值,打印[Function: a]
预编译发生在全局 (三部曲)
当预编译放生在全局中,编译过程是怎样的呢?
- 创建 GO 对象 (Global Object)
- 找变量声明,将变量声明作为GO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
我们以这个代码为例:
global = 100
function fn(){
console.log(global);
global = 200
console.log(global);
var global = 300
}
fn()
var global;
第一步:创建 GO 对象 (Global Object)
GO:{
}
第二步:找变量声明,将变量声明作为GO的属性名,值为undefined
GO:{
global: undefined
}
在全局中我们找到一个var global的变量声明,并将变量声明作为GO的属性名,值为undefined
第三步:在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
GO:{
global: undefined,
fn: function fn(){}
}
我们在全局中找到了function fn(){}这样一个函数声明,我们将函数名作为GO对象的属性名,值赋予函数体
到这里全局的预编译便结束了,执行引擎开始执行代码,第一行执行global = 100
GO对象变为
GO:{
global: 100,
fn: function fn(){}
}
第二行到第七行为函数的声明,不执行。到第八行函数在执行之前便会创建一个AO对象,按照刚刚了解的预编译发生在函数执行之前(四部曲),AO对象为:
AO:{
global: undefined
}
然后开始fn的执行
在fn中第一步执行console.log(global);
它便会在自己的执行上下文里找global,显然在AO对象中有global: undefined
,那么第一个console.log(global);
打印的值便为undefined,第二个console.log(global);
打印时,因为在这之前有一个global = 200
将AO对象中的global改为200,所以第二个console.log(global);
打印200,最后一个var global = 300
会将AO对象中的global值变为300