前言
在专栏《你不知道的JavaScript》中,也就是上一篇《"深入探索JavaScript:从词法分析到词法作用域的欺骗"》,深入探讨了编译过程:从词法分析->语法分析->代码生成,那么在JavaScript这门语言中这个编译过程是怎么样的呢?
比起传统的只有3个步骤的语言的编译器,JavaScript引擎要复杂的多,但总体来看,JavaScript编译过程只有下面三个步骤: 1. 语法分析 2. 预编译 3. 解释执行
引入面试题
执行结果是什么
javascript
function test(a, b) {
console.log(a); // 1
c = 0
var c;
a = 3
b = 2
console.log(b); // 2
function b() { }
console.log(b); //2
}
test(1)
分析过程
众所周知,如果你不了解编译原理,一定会以就近原则定论,那么接下来就让我们探讨一下原理,剖析完原理之后,再返回来看这道题目,届时你一定会豁然开朗。
在 JavaScript 中,预编译是在代码执行前进行的一项操作,它会把变量声明提前,函数声明也提前,把这些按照一定的规则,放在创建的对象里面去。这个过程主要包括以下几个步骤:
- 发生在全局
- 创建GO对象
- 找变量声明,将变量名作为GO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
- 发生在函数体内
- 创建 AO(Activation Object)对象:在函数执行前,会创建一个 AO 对象,用于存储函数内部的局部变量。
- 找形参和变量声明:在函数体内,找到形参和变量声明,并将变量和形参名作为 AO 属性名,值为 undefined
- 将实参和形参统一:将实参和形参统一到函数体中。
- 找函数声明:在函数体内,找到函数声明,并将函数值赋予函数体。
通过预编译,JavaScript 可以在函数执行前准备好所需的变量和函数,提高代码的执行效率。
我们一起来看这一份代码,理解了上述分析过程,那么在JavaScript引擎的眼里这个分析过程应该是这样的:
- 首先看见了一个函数,发生在全局,那么创建一个Global Object GO{ 找变量,没找到,找函数,找到了,因此在这个对象中就有属性test,值为:function(){}}
- 接下来就读到了调用,但是在调用之前,引擎需要知道要执行什么东西,因此要对这个函数进行预编译
- 发生在函数体内的预编译,建立一个AO对象{找形参、变量声明,值为undefined,因此就有:a:undefined,b:undefined,c:undefinded,然后将形参与实参统一,即a:1,接下来找函数声明,即b:function(){}},预编译完成之后就开始执行代码,此时AO{
a:1,
b:function(){},
c:undefined
} 引擎从上往下开始执行,1.执行打印a,a为1, 2.将0赋值给c,即c=0,a=3,b=2,此时AO{ c=0, a=3, b=2 },接下来就到了打印b的代码,即b=2,接下来又是一个打印b的代码,即b=2.
因此正确答案就是a=1,b=2,b=2
思考
如果JavaScript引擎在AO里面没找到要找的属性怎么办?他会去哪里找?
举个例子
ini
function a() {
function b() {
b = 22;
}
var a = 111;
b();
console.log(a);
}
var glob = 100;
a();
分析
在 JavaScript 中,作用域可以分为以下几种类型:
- 全局作用域:变量在整个脚本中都可访问。
- 函数作用域:变量仅在函数内部可访问。
作用域链的形成与函数的调用和嵌套有关。当函数被调用时,会创建一个新的作用域,并将其添加到作用域链中。函数内部的变量和函数可以访问外部作用域中的变量和函数,从而形成了作用域链的结构。
首先创建GO对象,里面有属性a,值为这个a的函数体,然后有属性glob,值为100。然后就碰到了调用a函数,此时JavaScript引擎就会去预编译a函数,创建AO对象,里面有属性a,值为111,属性b,值为这个function函数体,紧接着调用b函数,又创建一个AO对象,里面有b属性,值为22。
可以看见,首先a的作用域会先指向这个GO对象,他先创建GO对象。那么作用域0号位置,指向这个GO对象,紧接着,他又创建了AO对象,那么此时,a的作用域的0号位置就会优先指向这个AO对象,然后1号位置指向GO,后来引出了b,那么同理,b的作用域0号位置首先指向他自己创建的AO对象然后再指向引出他的a(1号位置)。
作用域链由一系列的作用域组成,每个作用域都包含了可访问的变量和函数。当代码中访问一个变量时,JavaScript 会沿着作用域链依次查找该变量。
那么在上述的这个分析过程,这么一个东西就是我们的作用域链了,大家可以通过画图理解。
由此我们就知道了,为什么内层函数可以访问外层,而外层函数无法访问到我们的内层。当JavaScript引擎没有在我们的AO对象中找到我们需要的属性时,他就会去下一个作用域找,也就是我们的GO对象中继续找。
小结
通过这篇文章,相信你对JavaScript预编译以及作用域又有了更加深刻的理解,当面试官给你写了一串代码,问你输出结果时,你能够冷静应对了吗?