参考文章:
1.《你不知道的JavaScript 上卷》
2. AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解

编译原理

我们先简单介绍一下在传统编译语言的流程中,程序中一段源代码在执行前会经历的三个步骤(统称为“编译”):

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

分词/词法分析就是将由字符组成的字符串(程序代码)分解成对编程语言来说有意义的代码块,这些代码块被称为词法单元(token)

我们直接举例子说明:

var a = 2;
复制代码

这段程序经过词法分析后通常会被分解成下面这些词法单元:var、a、=、2、;

空格是否被当作词法单元,取决于空格在这门语言中是否具有意义

2 解析/语法分析(Parsing)

解析/语法分析就是将词法单元流(数组)转换为一个由元素逐级嵌套所组成的代表了程序语法结构的树——抽象语法树(Abstract Syntax Tree, AST)

我们来看一下 var a = 2; 经过语法分析后生成的AST是什么样子的:

可以看到这段程序的类型是(这段代码是做什么的) VariableDeclaration(变量声明),下面我们来看一下 VariableDeclarator 的详细内容:

id: 代表变量的名称(标识符)—— a

init-Literal: 代表变量值 —— 2

如果想了解 AST,请查看 AST对象文档

3 代码生成

代码生成就是将AST转换为可执行代码(机器指令)的过程。简单来说就是将var a = 2; 的AST转为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存),并将一个值存储在 a 中。这个过程与语言、目标平台等息息相关。

浏览器引擎是如何执行 var a = 2; 的

准备工作

在讲正文之前,我们还需要先了解几个角色:

  • 引擎
    引擎爸爸的工作:负责整个 JavaScript 程序的编译及执行过程
  • 编译器
    引擎的好朋友,负责词法、语法分析及代码生成等脏活累活
  • 作用域
    引擎的另一个好朋友,负责收集并维护所有的声明(变量,函数),并实施一套严格的规则,确定当前执行的代码对这些声明的访问权限

执行过程

JavaScript 实际上是一门编译语言,但与传统的编译语言不同, 它不是提前编译的, 编译结果也不能在分布式系统中进行移植。

任何 JavaScript 代码片段在执行前都要进行编译( 通常就在执行前)。 因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译, 然后做好执行它的准备, 并且通常马上就会执行它。

编译过程

1. 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构(AST),然后开始进行代码生成。

2. 编译器进行代码生成时,遇到 var a ,编译器会询问作用域是否已经存在一个该名称的变量,如果是,编译器则忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。

3. 编译器为引擎生成运行时所需代码之后,引擎开始执行代码。

执行过程

引擎首先会询问作用域,在当前作用域是否存在一个叫作 a 的变量,如果是,引擎则会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了 a 变量,就会将 2 赋值给它,否则引擎会抛出一个异常。

引擎是如何查找变量的

  • LHS 查询
    如果查找的目的是对变量进行赋值,则使用 LHS 查询(告诉作用域我需要对 a 变量进行 LHS 引用,你见过它嘛?) 不成功的 LHS 引用会导致自动隐式创建一个全局变量(非严格模式),严格模式下抛出 ReferenceError 异常

  • RHS 查询
    如果查找的目的是获取变量的值,则使用 RHS 查询 不成功的 RHS 查询会抛出 ReferenceError 异常

function add(a, b) {
    return a + b;
}
add(1, 2)

// add() -- RHS 一次
// 参数赋值a = 1, b = 2 -- 隐式 LHS 两次
// a + b -- RHS 两次
// LHS 共两次, RHS 共三次
复制代码

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