【前言】
本文简要的总结一下在JavaScript的学习中对提升这个概念的理解。有不同意见的朋友可以在评论区留言,大家一起交流讨论。
【正文】
众所周知,在传统编译语言的运行流程中,源代码会经历“词法分析”、“语法分析”、“代码生成”三个步骤,统称为编译。而在JavaScript中,存在一个概念叫做预编译,预编译分为全局预编译和函数预编译,全局预编译发生在页面加载完成时执行,而函数预编译发生在函数执行的前一刻。其实我们在这说的预编译的说法并不太准确,在《JavaScript的秘密花园》中提到variable hosting,也就是var和function的提升问题,就是描述这个过程的。但将这个过程叫做预编译是有利于我们理解的。接下来进入正题。
函数预编译
全局预编译,分为四个步骤,它发生在函数体执行之前,我们在这称它为四部曲。
-
创建AO对象(activation object)
-
找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
-
将实参和形参统一
-
在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
单独列出来,大家可能很难理解,这不打紧,因为我也是哈哈,接下来让咱们引入代码进行具体分析
` var a = 1
function fn(a) {
var a = 2
function a() {}
var b = a
console.log(a); // 2
}
fn(3)
`
复制代码
现在咱们按着四部曲依次操作代码。①首先创建一个AO对象
` AO
{
}`
复制代码
②现在咱们去寻找形参和变量声明,咱们找到了a和b,并对它们赋值为undefined
> AO: {
> a: undefined
> b: undefined
> }
复制代码
③接着把实参形参统一
>
> AO: {
> a: 3
> b: undefined
> }
复制代码
④在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体 ,我们找到了function a()
>
> AO: {
> a: function a() {}
> b: undefined
> }
复制代码
那么此时的函数体预编译过程就结束了,此时函数体在正式运行前,拿到的就是AO中的值。接下来就正常运行,该覆盖的覆盖。这里放一道例题给大家练习一下:
> function fn(a) {
> console.log(a); // function() {}
> var a = 123;
> console.log(a); // 123
> function a() {}
> console.log(a); // 123
> var b = function() {}
> console.log(b); // function() {}
> function d() {}
> var d = a
> console.log(d); // 123
> }
> fn(1)
复制代码
请问此时第二行代码中的console.log(a)会输出什么呢?按我们的预编译过程推出的是输出function() {},我们在这里贴上预编译的结果:
> AO: {
> a: undefined 1 function() {} ,
> b: undefined function() {},
> d: undefined function () {}
> }
>
复制代码
可能有人觉得这不变量提升就能解决的么,但我们要知道少数几串代码你可能可以想出来,当几十行代码摆你面前时,选择这种方法会更加的高效。
全局预编译
全局预编译分为三个步骤,但是要注意,如果过程中有函数,那么我们也要对函数进行预编译。
- 创建一个GO对象
- 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
- 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体
var global = 100 function fn() { console.log(global); // undefined global = 200 console.log(global); // 200 var global = 300 } fn() 复制代码
方法与上文中的函数预编译大体相似,只是少了一个寻找形参和实参、形参统一的过程,在这里我们直接贴结果,由于这串代码中有函数,所以我们也对其进行了预编译
`
GO: {
global:undefined
fn: function() {}
}
AO: {
global: undefined
}`
复制代码
此时我们的第三行代码console.log(global)会输出undefined,这是预编译的结果告诉我们的,那么此时我们小小的变换一下。将第一行的var global = 100换成global = 100,我们现在想想,现在的全局预编译中的GO对象是什么?console.log(global);又会输出什么呢?来,让我们看结果:
` GO: {
global:undefined
fn: function() {}
}
console.log(global); // undefined
复制代码
是的,它的值还是一样。看到这里,可能很多人会感到疑惑,不是说全局预编译的第二步要找变量声明么?可是此时我们并没有声明global啊,为什么在全局预编译中 var global = 100得到的结果和 global = 100是一样的呢?
此时我们就要引用一个新的概念,RHS查询和LHS查询
RHS查询和LHS查询
拿var a=2来进行分析;
先让我们来分析一下编译过程,我们的编译器拿到这段程序时,会先将其分解为词法单元,然后将词法单元解析成一个树结构,也就是抽象语法树,简称为AST。接下来就是将AST转换为机器指令也就是将其变为可执行代码。那么当我们的引擎去执行这段代码时,它会先通过查找变量a来判断它是否已经声明过,查找需要作用域的协助,但是引擎执行查找的方式会影响最终的查找结果。
引擎查找分为RHS和LHS,R和L的意思就是右和左的意思,也就是说当变量出现的赋值操作的左侧时会进行LHS查询,在右侧时会进行RSH查询。RHS查询与简单地查找某个变量的值一样,也就是得到某某值,LHS查询是试图找到变量的容器本身,从而对其赋值。这里用代码的例子方便理解
`console.log(a);`
复制代码
这里的语句需要查找到a的值,所以是RHS查找
`a=2`
复制代码
这里我们其实并不关心a=2是什么,只是想为=2这个赋值操作找到一个目标而已
按我个人的记忆法就是,类似于console这种直接找赋值操作的源头,也就是找一个值得时候一般都是RHS引用,而当出现了=号时,也就是要找到赋值操作得目标时,一般都是LHS引用。
无论是RHS查找还是LHS查找都会从当前执行的作用域中开始,如果在当前作用域没有找到所需要的标识符,它们会逐级向上层作用域查找目标标识符,直到到达了全局作用域,此时无论找到或者没有找到都会停止。
RHS和LHS不同得地方在于,如果RHS引用不成功会抛出ReferenceError异常。而LHS则是会自动隐式得创建一个全局变量(非严格模式下,严格模式就歇逼了)也就是说 假如a=2中的a在作用域中并未声明,那么在引用LHS时就会创建一个a出来,这个a是全局变量。即没有枪没有炮,咱们自己造。
所以我们此时回到代码当中来看
global = 100
function fn() {
console.log(global); // undefined
global = 200
console.log(global); // 200
var global = 300
}
fn()
复制代码
此时得global并未进行声明,这时我们对其进行了LHS引用,在全局作用域中我们都没有找到目标标识符,这时就自动隐式的创建了一个global变量,可以理解为偷摸的执行了一段var global=100,所以在咱们执行全局预编译三部曲时,可以找到global的变量声明。这也就解释了为何两行不同的代码,结果都一样。
注::本文思想来自《你不知道的JavaScript》