编译原理

前言

不知你是否想过,我们在编辑器中所写的 JavaScript 代码是如何在机器上运行的,我们随心所欲写的代码又如何被机器准确无误的识别。要了解这些,我们不得不先了解下编译原理。

编译原理

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历词法分析、语法分析、代码生成三个步骤,统称为“编译”。我们知道 JavaScript 实际上是一门编译语言。JavaScript 引擎进行编译的的步骤和传统的编译语言非常相似。

 

  • 词法分析

我们在编辑器编写的代码,实际上就是由字符组成的字符串。在这个过程会将这些字符串分解成有意义的词法单元(token)。

例如,我们编写的 var a = 1; 这段程序会被分解成这些词法单元:var、a、=、1、;。空格是否会被当成词法单元,取决于空格在这门语言中是否具有意义。

  • 语法分析

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

  • 代码生成

 将AST转化为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等有关。简单的说就是由某种办法将AST转化为一组机器指令,由机器执行。

 

比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂的多,像在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

 

 

抽象语法树(AST)

好了,我们现在初步理解了编译的过程,那么在 JavaScript 中它的应用场景呢。这里要着重关注下在词法分析中生成的抽象语法树(AST),在现代前端工程之中,到处都有它的身影,只不过其背后的原理对于大部分开发者还属于黑盒。 像 eslint jshint stylelint css-in-js prettier jsx vue-template uglify-js postcss less babel 等等,从模板到代码检测,从混淆压缩到代码转换,甚至编辑器的代码检查和高亮都与之相关。而这些应用工具的工作原理都可以分为三个部分。

  • 解析: 将代码(其实就是字符串)转换成 AST( 抽象语法树)
  • 转换: 访问 AST 的节点进行变换操作生成新的 AST
  • 生成: 以新的 AST 为基础生成代码

一些例子

上面了解到通过 AST 我们可以做很多的事情,下面我们来看看具体的例子

例1:转化箭头函数

我们知道现在开发都用 ES6 语法来开发,但是是需要转化成 ES5 语法代码的,这样才能兼容各种浏览器版本。下面我们利用 babel-core 、 babel-types 、 AST 来将 ES6 中的箭头函数转化为 ES5 语法的形式。

var sum = (a, b) => a + b

// 转化为

var sum = function(a, b) {
  return a + b
}

实现代码如下:

// babel核心库,实现核心的转换引擎
var babel = require('babel-core');
// 可以实现类型判断,生成AST节点等
var types = require('babel-types');

var code = `var sum = (a, b) => a + b`;
// var sum = function(a, b) {
//   return a + b
// };

// 这个访问者会遍历AST节点,对特定类型的节点进行转化处理
var visitor = {
  ArrowFunctionExpression(path) {
    console.log(path.type);
    var node = path.node;
    var expression = node.body;
    var params = node.params;
    var returnStatement = types.returnStatement(expression);
    var block = types.blockStatement([
        returnStatement
    ]);
    var func = types.functionExpression(null, params, block, false, false);
    path.replaceWith(func);
  }
}

var arrayPlugin = { visitor }
// babel内部会把代码先转成AST, 然后进行遍历
var result = babel.transform(code, {
  plugins: [
    arrayPlugin
  ]
})
console.log(result.code);
// var sum = function (a, b) {
//   return a + b;
// };

 

例2:代码混淆

var UglifyJS = require('uglify-js');
var code = 'var a = 1;';
var toplevel = UglifyJS.parse(code); // 解析成 AST
var transformer = new UglifyJS.TreeTransformer(function (node) {
  if (node instanceof UglifyJS.AST_Number) { // AST_Number 类型的节点
    node.value = '0x' + Number(node.value).toString(16); // 改成 16 进制
    return node; // 返回新的节点
  };
});
toplevel.transform(transformer);  // 遍历 AST 树
var ncode = toplevel.print_to_string(); // 将转化后的 AST 还原成目标代码
console.log(ncode); // var a = 0x1;
 
首先通过 uglify-js 模块的 parse 方法将源代码解析成 AST。
然后通过 TreeTransformer 遍历 AST,当遇到类型为 AST_Number(ast)的节点时,将节点的 value 值改成 16 进制,然后 return node 就会用新的节点代替原节点,我们就得到转化后的 AST。
最后调用 print_to_string 将转化后的 AST 还原成代码字符串。
我们就得到了混淆之后的目标代码。
 

更多精彩内容,欢迎关注微信公众号~

  
posted @ 2020-02-02 21:28  阿林十一  阅读(567)  评论(0编辑  收藏  举报