◆

JavaScript 编译原理详解:解析、转换与代码生成

2023-02-02 01:370

通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。

编译器工作流

  • 解析(Parsing): 解析是将最初原始的代码转换为一种更加抽象的表示(即 AST)
  • 转换(Transformation): 转换将对这个抽象的表示做一些处理,让它能做到编译器期望它做到的事情
  • 代码生成(Code Generation): 接收处理之后的代码表示,然后把它转换成新的代码

解析

  • 词法分析:接收原始代码,然后把它分割成一些被称为 token 的东西,这个过程是在词法分析器(Tokenizer 或者 Lexer)中完成的 Token 是一个数组,由一些代码语句的碎片组成。它们可以是数字、标签、标点符号、运算符或者其它任何东西
  • 语法分析:接收之前生成的 token,把它们转换成一种抽象的表示,这种抽象的表示描述了代码语句中的每一个片段以及它们之间的关系。 这被称为中间表示(intermediate representation)或抽象语法树(Abstract Syntax Tree, 缩写为 AST)

示例

<h1 id="title">
  <span>hello</span>
  world
</h1>

转换后的 tokens

[
  { type: 'Punctuator', value: '<' },
  { type: 'JSXIdentifier', value: 'h1' },
  { type: 'JSXIdentifier', value: 'id' },
  { type: 'Punctuator', value: '=' },
  { type: 'String', value: '"title"' },
  { type: 'Punctuator', value: '>' },
  { type: 'Punctuator', value: '<' },
  { type: 'JSXIdentifier', value: 'span' },
  { type: 'Punctuator', value: '>' },
  { type: 'JSXText', value: 'hello' },
  { type: 'Punctuator', value: '<' },
  { type: 'Punctuator', value: '/' },
  { type: 'JSXIdentifier', value: 'span' },
  { type: 'Punctuator', value: '>' },
  { type: 'JSXText', value: 'world' },
  { type: 'Punctuator', value: '<' },
  { type: 'Punctuator', value: '/' },
  { type: 'JSXIdentifier', value: 'h1' },
  { type: 'Punctuator', value: '>' },
];

转换成 AST

{
  'type'
:
  'Program',
    'body'
:
  [
    {
      'type': 'ExpressionStatement',
      'expression': {
        'type': 'JSXElement',
        'openingElement': {
          'type': 'JSXOpeningElement',
          'name': {
            'type': 'JSXIdentifier',
            'name': 'h1'
          },
          'attributes': [
            {
              'type': 'JSXAttribute',
              'name': {
                'type': 'JSXIdentifier',
                'name': 'id'
              },
              'value': {
                'type': 'Literal',
                'value': 'title'
              }
            }
          ]
        },
        'children': [
          {
            'type': 'JSXElement',
            'openingElement': {
              'type': 'JSXOpeningElement',
              'name': {
                'type': 'JSXIdentifier',
                'name': 'span'
              }
              'attributes': []
            },
            'children': [
              {
                'type': 'JSXText',
                'value': 'hello'
              }
            ],
            'closingElement': {
              'type': 'JSXClosingElement',
              'name': {
                'type': 'JSXIdentifier',
                'name': 'span'
              }
            }
          },
          {
            'type': 'JSXText',
            'value': 'world'
          }
        ],
        'closingElement': {
          'type': 'JSXClosingElement',
          'name': {
            'type': 'JSXIdentifier',
            'name': 'h1'
          }
        }
      }
    }
  ]
}

转换

  • 编译器的下一步就是转换,它只是把 AST 拿过来然后对它做一些修改。它可以在同种语言下操作 AST,也可以把 AST 翻译成全新的语言
  • AST 中有很多相似的元素,这些元素都有 type 属性,它们被称为 AST 结点。这些结点含有若干属性,可以用于描述 AST 的部分信息
  • 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新的 AST
  • 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对新语言的全新的 AST

代码生成

  • 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,但是代码生成最主要的部分还是根据 AST 来输出代码
  • 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码表示,以便于线性地输出代码

传统编译语言的流程

一般会经历三个步骤,统称为“编译”。

分词/词法分析

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

解析/语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树 ”(Abstract Syntax Tree,AST)。 var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下 来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子节点。

代码生成

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

作用域

JavaScript 的编译过程不是发生在构建之前的。

对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。

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

  • 引擎

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

  • 编译器

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

  • 作用域

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

用来管理引擎如何在当前作用 域以及嵌套的子作用域中根据标识符名称进行变量查找。。

javascript 对 var a = 2 这段代码的编译运行分析

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构,也就是上面提到的分词以及解析。

编译器开始进行代码生成时的处理如下

  • 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
  • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会抛出一个异常!

总结

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

引擎对变量的查找

在 var a = 2 这段代码中,引擎会为变量 a 进行 LHS 查询。另外一个查找的类型叫作 RHS。

LHS 与 RHS 查询相关

LHS 与 RHS 查询

tip

本文参考来自:你不知道的 javascript

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript 编译原理详解:解析、转换与代码生成加载中