关于var a = 2; JS干了什么?

2,966 阅读5分钟

又是一年临近年底了,年底制定下了许多计划,正在一点一点实现,最近在开始读《你不知道的Javascript》了,也会慢慢把读书笔记通过博客的形式输出出来,让自己印象更深刻,今天就来聊聊JS中的var a = 2;这行代码发生了什么?

编译

对于编程语言来说都会有一个编译的过程,一段代码在执行前大多都会经历下面几个步骤:(具体的细节会根据语言特性而异)

  • 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2 、;。

  • 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

  • 代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息 息相关。 抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指 令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

角色

  • 引擎

从头到尾负责整个 JavaScript 程序的编译及执行过程。

  • 编译器

负责语法分析及代码生成等

  • 作用域

负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

执行

我们在了解了这些编译中的基本概念后,我们在来仔细看看var a = 2;这行代码发生了什么;

  • 首先编译器遇到这句话时会先将这段程序分解成词法单元,然后把词法单元解析成一个树结构也就是AST,然后进行代码生成
  • 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。(变量声明阶段)
  • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了 a 变量,就会将 2赋值给它。否则引擎就会举手示意并抛出一个异常 (变量赋值阶段)

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

编译器查找变量

我们知道了var a = 2;js的执行过程了,那么如果是var a = b;呢? 那么这个过程对于b来说我们如何查到变量b的值呢;

编译器查找变量有两种类型,一直是LHS,一种是RHS,举个例子:

a = 2

这个例子就是LHS查询,当编译器去查找a变量的时候,其实是为了找到a变量的容器,去给a赋值,所以LHS查询其实是查询变量的容器

console.log(a)

这个例子中的其实就是RHS查询,当编译器去查找a变量的时候,其实目的是找到a变量的值而不是他的容器,因此RHS查询其实是查询变量的值

ok,我们知道了之后我们来尝试一下一个进阶例子。

function foo(a) { // 隐式的进行参数赋值 将a = 2; 进行LHS查询 查询到a容器 并进行赋值
 var b = a; // RHS进行查询 查询到a变量的值 然后 LHS查询 查询到b容器并进行赋值
 return a + b;// RHS进行查询 查询到a和b变量的值
}
var c = foo( 2 ); // 编译器先RHS查找foo变量 然后进入到foo函数体 函数执行完成后,RHS查询查询变量c的容器并进行赋值

异常

  • 当进行RHS变量查找的时候,如果沿着作用域链一直查找 并没有找到变量的时候,这个时候会抛出ReferrenceError
  • 当进行LHS变量查找的时候,如果沿着作用域链一直查找 并没有找到变量的时候,如果是在非严格模式下 那么就会在全局作用域中创建一个这样的变量并且进行赋值操作,如果是在严格模式下也会抛出ReferrenceError的错误

注:如果RHS查到了一个变量,但是这个变量的类型和我们将要进行的操作不同时,比如试图对一个非函数类型的值进行函数调用,那么就会抛出TypeError的错误