漫谈从文本到数据结构

前言

像我这样没有经历科班的计算机基础教育的开发者,每当和人讨论到类型,编译器等这些概念的时候,很快就会变得不知所措起来。所以我也一直在花时间尝试加强自己的计算机基础知识,这篇文章就是一些记录,来帮助自己理清某些相关的概念。

解释型 or 编译型 or ?

JavaScript的学习者是幸福的,只需要一个编辑器和浏览器就可以让自己的代码立马跑起来,我们在学习其他的诸如c/cpp/go等等语言的时候往往还需要先搭建环境,写完代码后交给编译器来进行编译。

上图从上至下分别是编译型语言,解释型语言和Java的执行模型。对于C和C++,它们经过一次编译之后,可以由操作系统直接执行,所以它们是编译型语言。而Java首先由编译器编译成.class(字节码)文件,然后在通过JVM从.class文件中读一行解释执行一行,所以对于它到底是解释型的语言还是编译型语言也有争议,但本文我们就不过多阐述了。

解释型语言的典型例子是 bash 脚本。我们终端中的 bash 解释器逐行读取我们的命令并且执行它,在之前的文章中我也简单总结过shell脚本编写的一些规范。

而JavaScript在我们的日常开发中往往并没有编译的这一基本步骤(先不考虑目前的构建打包工具等),我们的js代码放在script标签中就可以直接用浏览器打开运行了。但是js中存在的诸如变量声明提升(hoisting)等特性,是简单的逐行执行解释器办不到的,实际上我们的代码在执行之前就被所谓的 “JavaScript 引擎” 或者是 “特定的编译环境” 编译过。这个编译的过程取决于具体的实现(比如,使用 V8 引擎的 node.js 和 Chrome 就和使用 SpiderMonkey 的 FireFox)。

Google 在 V8 中引入了 JIT 技术 (Just in time compiling 即时编译),使得Javascript 瞬间提升了非常多的速度,也直接导致了越来越多的大型网页应用的出现和前端工程师这一细分职业的诞生和发展。

这里我们不过多深入编译型执行和解释型执行,实际上现在很多对比编译程序与解释程序都开始逐渐模糊。但我们的代码实际上总是需要转换成低级的机器码(也许你并看不见这一步骤)才能被计算机正确的执行。

run time and compile time

理解了编译和运行是两个不同的阶段,那 “运行阶段 Run Time” 和 “编译阶段 Compile Time” 理解起来也就容易多了。

编译阶段,就是我们在我们的编辑器或者 IDE 当中的代码转换成其它格式的代码的阶段。

运行阶段,就是我们程序实际执行的阶段。

我们以普通文本形式编写我们的程序,因为这是我们人类与计算机交互的最好方式,但对于计算机来说,普通的文本实际上它是根本无法理解的,因此编译阶段就是将普通的文本交给编译器来处理成机器可以理解的形式,在编译器里如何处理文本呢,这也是一个非常棘手的事情。它可能包含程序运作不必要的东西,例如空格,换行,或者可能存在有歧义的部分。

对于文本的解析,在之前的JSON解析器中我们使用循环来逐字解析,在爬虫提取信息时我们会使用到正则表达式,但对于一个庞大的程序,如果只使用这两种方法那无疑将非常痛苦并且效率低下。

之前提过几乎所有的信息都可以用JSON这种数据结构来表达(当然在不同的语言中这种树形结构的名字并不相同),程序其实也不例外。因此,我们希望将我们的程序转换成数据结构以方便操作。这个数据结构就是 AST(Abstract Syntax Tree)。

AST 可以通过多种不同的方式表示,这里有一个很方便的网站可以从js代码来生成AST方便我们理解。比如最简单的一句声明变量代码:

var a = 1

转换后的结果为:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

可以看到我们能够通过操纵语法树,精准的从AST中获得程序代码中的某个节点,而这也是用正则表达式所不能准确体现的地方。抽象语法树的作用非常的多,比如编译器、IDE、压缩优化代码等。在JavaScript中,虽然我们并不会常常与AST直接打交道,但却也会经常的涉及到它。例如使用UglifyJS来压缩代码,模版引擎中变量的绑定替换。

在有了nodejs后,越来越多的提升效率的工具开始出现,比如将浏览器未广泛支持的es6/7/8语法转换为es5或es3语法的babel,比如我们可以使用sass,less等扩展型的css语言来开发,然后将其编译为浏览器可识别的css语言,js本身也出现了typescript,flow等超集语言,在开发时就能实现静态类型注释等特性,这些都离不开AST这一概念。