阅读 1389

从LHS和RHS查询的角度看待JavaScript中的预编译

【前言】

本文简要的总结一下在JavaScript的学习中对提升这个概念的理解。有不同意见的朋友可以在评论区留言,大家一起交流讨论。

【正文】

众所周知,在传统编译语言的运行流程中,源代码会经历“词法分析”、“语法分析”、“代码生成”三个步骤,统称为编译。而在JavaScript中,存在一个概念叫做预编译,预编译分为全局预编译和函数预编译,全局预编译发生在页面加载完成时执行,而函数预编译发生在函数执行的前一刻。其实我们在这说的预编译的说法并不太准确,在《JavaScript的秘密花园》中提到variable hosting,也就是var和function的提升问题,就是描述这个过程的。但将这个过程叫做预编译是有利于我们理解的。接下来进入正题。

函数预编译

全局预编译,分为四个步骤,它发生在函数体执行之前,我们在这称它为四部曲。

  1. 创建AO对象(activation object)

  2. 找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined

  3. 将实参和形参统一

  4. 在函数体里找函数声明,将函数名作为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 () {}   
>      }
> 
复制代码

可能有人觉得这不变量提升就能解决的么,但我们要知道少数几串代码你可能可以想出来,当几十行代码摆你面前时,选择这种方法会更加的高效。

全局预编译

全局预编译分为三个步骤,但是要注意,如果过程中有函数,那么我们也要对函数进行预编译。

  1. 创建一个GO对象
  2. 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
  3. 找全局里的函数声明,将函数名作为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》

文章分类
前端
文章标签