JavaScript 语法树与代码转化实践 归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章中。本文引用的参考资料声明于 JavaScript 学习与实践资料索引中,特别需要声明是部分代码片引用自 Babel Handbook 开源手册;也欢迎关注前端每周清单系列获得一手资讯。

JavaScript 语法树与代码转化

浏览器的兼容性问题一直是前端项目开发中的难点之一,往往客户端浏览器的升级无法与语法特性的迭代保持一致;因此我们需要使用大量的垫片(Polyfill),以保证现代语法编写而成的 JavaScript 顺利运行在生产环境下的浏览器中,从而在可用性与代码的可维护性之间达成较好的平衡。而以 Babel 为代表的语法转化工具能够帮我们自动将 ES6 等现代 JavaScript 代码转化为可以运行在旧版本浏览器中的 ES5 或其他同等的实现;实际上,Babel 不仅仅是语法解析器,其更是拥有丰富插件的平台,稍加扩展即可被应用在前端监控埋点、错误日志收集等场景中。笔者也利用 Babel 以及 Babylon 为 swagger-decorator 实现了 flowToDecorator 函数,其能够从 Flow 文件中自动提取出类型信息并为类属性添加合适的注解。

Babel

自 Babel 6 之后,核心的 babel-core 仅暴露了部分核心接口,并使用 Babylon 进行语法树构建,即上图中的 Parse 与 Generate 步骤;实际的转化步骤则是由配置的插件(Plugin)完成。而所谓的 Preset 则是一系列插件的合集,譬如 babel-preset-es2015 的源代码中就定义了一系列的插件:

  return {plugins: [[transformES2015TemplateLiterals, { loose, spec }],transformES2015Literals,transformES2015FunctionName,[transformES2015ArrowFunctions, { spec }],transformES2015BlockScopedFunctions,[transformES2015Classes, optsLoose],transformES2015ObjectSuper,...modules === "commonjs" && [transformES2015ModulesCommonJS, optsLoose],modules === "systemjs" && [transformES2015ModulesSystemJS, optsLoose],modules === "amd" && [transformES2015ModulesAMD, optsLoose],modules === "umd" && [transformES2015ModulesUMD, optsLoose],[transformRegenerator, { async: false, asyncGenerators: false }]].filter(Boolean) // filter out falsy values};

Babel 能够将输入的 JavaScript 代码根据不同的配置将代码进行适当地转化,其主要步骤分为解析(Parse)、转化(Transform)与生成(Generate):

  • 在解析步骤中,Babel 分别使用词法分析(Lexical Analysis)与语法分析(Syntactic Analysis)来将输入的代码转化为抽象语法树;其中词法分析步骤会将代码转化为令牌流,而语法分析步骤则是将令牌流转化为语言内置的 AST 表示。

  • 在转化步骤中,Babel 会遍历上一步生成的令牌流,根据配置对节点进行添加、更新与移除等操作;Babel 本身并没有进行转化操作,而是依赖于外置的插件进行实际的转化。

  • 最后的代码生成则是将上一步中经过转化的抽象语法树重新生成为代码,并且同时创建 SourceMap;代码生成相较于前两步会简单很多,其核心思想在于深度优先遍历抽象语法树,然后生成对应的代码字符串。

抽象语法树

抽象语法树(Abstract Syntax Tree, AST)的作用在于牢牢抓住程序的脉络,从而方便编译过程的后续环节(如代码生成)对程序进行解读。AST 就是开发者为语言量身定制的一套模型,基本上语言中的每种结构都与一种 AST 对象相对应。上文提及的解析步骤中的词法分析步骤会将代码转化为所谓的令牌流,譬如对于代码 n * n,其会被转化为如下数组:

[{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },...
]

其中每个 type 是一系列描述该令牌属性的集合:

{type: {label: 'name',keyword: undefined,beforeExpr: false,startsExpr: true,rightAssociative: false,isLoop: false,isAssign: false,prefix: false,postfix: false,binop: null,updateContext: null},...
}

这里的每一个 type 类似于 AST 中的节点都拥有 startendloc 等属性;在实际应用中,譬如对于 ES6 中的箭头函数,我们可以通过 babylon 解释器生成如下的 AST 表示:

// 源代码
(foo, bar) => foo + bar;// 简化的 AST 表示
{"program": {"body": [{"type": "ExpressionStatement","expression": {"type": "ArrowFunctionExpression","params": [{"type": "Identifier","name": "foo"},{"type": "Identifier","name": "bar"}],"body": {"type": "BinaryExpression","left": {"type": "Identifier","name": "foo"},"operator": "+","right": {"type": "Identifier","name": "bar"}}}}]}
}

我们可以使用 AST Explorer 这个工具进行在线预览与编辑;在上述的 AST 表示中,顾名思义,ArrowFunctionExpression 就表示该表达式为箭头函数表达式。该函数拥有 foo 与 bar 这两个参数,参数所属的 Identifiers 类型是没有任何子节点的变量名类型;接下来我们发现加号运算符被表示为了 BinaryExpression 类型,并且其 operator 属性设置为 +,而左右两个参数分别挂载于 leftright 属性下。在接下来的转化步骤中,我们即是需要对这样的抽象语法树进行转换,该步骤主要由 Babel Preset 与 Plugin 控制;Babel 内部提供了 babel-traverse 这个库来辅助进行 AST 遍历,该库还提供了一系列内置的替换与操作接口。而经过转化之后的 AST 表示如下,在实际开发中我们也常常首先对比转化前后代码的 AST 表示的不同,以了解应该进行怎样的转化操作:

// AST shortened for clarity
{"program": {"type": "Program","body": [{"type": "ExpressionStatement","expression": {"type": "Literal","value": "use strict"}},{"type": "ExpressionStatement","expression": {"type": "FunctionExpression","async": false,"params": [{"type": "Identifier","name": "foo"},{"type": "Identifier","name": "bar"}],"body": {"type": "BlockStatement","body": [{"type": "ReturnStatement","argument": {"type": "BinaryExpression","left": {"type": "Identifier","name": "foo"},"operator": "+","right": {"type": "Identifier","name": "bar"}}}]},"parenthesizedExpression": true}}]}
}

自定义插件

Babel 支持以观察者(Visitor)模式定义插件,我们可以在 visitor 中预设想要观察的 Babel 结点类型,然后进行操作;譬如我们需要将下述箭头函数源代码转化为 ES5 中的函数定义:

// Source Code
const func = (foo, bar) => foo + bar;// Transformed Code
"use strict";
const _func = function(_foo, _bar) {return _foo + _bar;
};

在上一节中我们对比过转化前后两个函数语法树的差异,这里我们就开始定义转化插件。首先每个插件都是以 babel 对象为输入参数,返回某个包含 visitor 的对象的函数。最后我们需要调用 babel-core 提供的 transform 函数来注册插件,并且指定需要转化的源代码或者源代码文件:

// plugin.js 文件,定义插件
import type NodePath from "babel-traverse";export default function(babel) {const { types: t } = babel;return {name: "ast-transform", // not requiredvisitor: {Identifier(path) {path.node.name = `_${path.node.name}`;},ArrowFunctionExpression(path: NodePath<BabelNodeArrowFunctionExpression>, state: Object) {// In some conversion cases, it may have already been converted to a function while this callback// was queued up.if (!path.isArrowFunctionExpression()) return;path.arrowFunctionToExpression({// While other utils may be fine inserting other arrows to make more transforms possible,// the arrow transform itself absolutely cannot insert new arrow functions.allowInsertArrow: false,specCompliant: !!state.opts.spec});}}};
}// babel.js 使用插件
var babel = require('babel-core');
var plugin= require('./plugin');var out = babel.transform(src, {plugins: [plugin]
});

常用转化操作

遍历

  • 获取子节点路径
    我们可以通过 path.node.{property} 的方式来访问 AST 中节点属性:

// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {path.node.left;path.node.right;path.node.operator;
}

我们也可以使用某个路径对象的 get 方法,通过传入子路径的字符串表示来访问某个属性:

BinaryExpression(path) {path.get('left');
}
Program(path) {path.get('body.0');
}
  • 判断某个节点是否为指定类型

  1. 内置的 type 对象提供了许多可以直接用来判断节点类型的工具函数:

BinaryExpression(path) {if (t.isIdentifier(path.node.left)) {// ...}
}

或者同时以浅比较来查看节点属性:

BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n" })) {// ...}
}// 等价于
BinaryExpression(path) {if (path.node.left != null &&path.node.left.type === "Identifier" &&path.node.left.name === "n") {// ...}
}
  • 判断某个路径对应的节点是否为指定类型

BinaryExpression(path) {if (path.get('left').isIdentifier({ name: "n" })) {// ...}
}
  • 获取指定路径的父节点
    有时候我们需要从某个指定节点开始向上遍历获取某个父节点,此时我们可以通过传入检测的回调来判断:

path.findParent((path) => path.isObjectExpression());// 获取最近的函数声明节点
path.getFunctionParent();
  • 获取兄弟路径
    如果某个路径存在于 Function 或者 Program 中的类似列表的结构中,那么其可能会包含兄弟路径:

// 源代码
var a = 1; // pathA, path.key = 0
var b = 2; // pathB, path.key = 1
var c = 3; // pathC, path.key = 2// 插件定义
export default function({ types: t }) {return {visitor: {VariableDeclaration(path) {// if the current path is pathApath.inList // truepath.listKey // "body"path.key // 0path.getSibling(0) // pathApath.getSibling(path.key + 1) // pathBpath.container // [pathA, pathB, pathC]}}};
}
  • 停止遍历
    部分情况下插件需要停止遍历,我们此时只需要在插件中添加 return 表达式:

BinaryExpression(path) {if (path.node.operator !== '**') return;
}

我们也可以指定忽略遍历某个子路径:

outerPath.traverse({Function(innerPath) {innerPath.skip(); // if checking the children is irrelevant},ReferencedIdentifier(innerPath, state) {state.iife = true;innerPath.stop(); // if you want to save some state and then stop traversal, or deopt}
});

操作

  • 替换节点

// 插件定义
BinaryExpression(path) {path.replaceWith(t.binaryExpression("**", path.node.left, t.numberLiteral(2)));
}// 代码结果function square(n) {
-   return n * n;
+   return n ** 2;}
  • 将某个节点替换为多个节点

// 插件定义
ReturnStatement(path) {path.replaceWithMultiple([t.expressionStatement(t.stringLiteral("Is this the real life?")),t.expressionStatement(t.stringLiteral("Is this just fantasy?")),t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),]);
}// 代码结果function square(n) {
-   return n * n;
+   "Is this the real life?";
+   "Is this just fantasy?";
+   "(Enjoy singing the rest of the song in your head)";}
  • 将某个节点替换为源代码字符串

// 插件定义
FunctionDeclaration(path) {path.replaceWithSourceString(`function add(a, b) {return a + b;}`);
}// 代码结果
- function square(n) {
-   return n * n;
+ function add(a, b) {
+   return a + b;}
  • 插入兄弟节点

// 插件定义
FunctionDeclaration(path) {path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}// 代码结果
+ "Because I'm easy come, easy go.";function square(n) {return n * n;}
+ "A little high, little low.";
  • 移除某个节点

// 插件定义
FunctionDeclaration(path) {path.remove();
}// 代码结果
- function square(n) {
-   return n * n;
- }
  • 替换节点

// 插件定义
BinaryExpression(path) {path.parentPath.replaceWith(t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me.")));
}// 代码结果function square(n) {
-   return n * n;
+   "Anyway the wind blows, doesn't really matter to me, to me.";}
  • 移除某个父节点

// 插件定义
BinaryExpression(path) {path.parentPath.remove();
}// 代码结果function square(n) {
-   return n * n;}

作用域

  • 判断某个局部变量是否被绑定:

FunctionDeclaration(path) {if (path.scope.hasBinding("n")) {// ...}
}FunctionDeclaration(path) {if (path.scope.hasOwnBinding("n")) {// ...}
}
  • 创建 UID

FunctionDeclaration(path) {path.scope.generateUidIdentifier("uid");// Node { type: "Identifier", name: "_uid" }path.scope.generateUidIdentifier("uid");// Node { type: "Identifier", name: "_uid2" }
}
  • 将某个变量声明提取到副作用中

// 插件定义
FunctionDeclaration(path) {const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);path.remove();path.scope.parent.push({ id, init: path.node });
}// 代码结果
- function square(n) {
+ var _square = function square(n) {return n * n;
- }
+ };

JavaScript 语法树与代码转化实践相关推荐

  1. 抽象语法树在 JavaScript 中的应用

    抽象语法树是什么 在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语 ...

  2. 编译原理抽象语法树_平衡抽象原理

    编译原理抽象语法树 使代码复杂易读和理解的一件事是,方法内部的指令处于不同的抽象级别. 假设我们的应用程序仅允许登录用户查看其朋友的旅行. 如果用户不是朋友,则不会显示任何行程. 一个例子: publ ...

  3. JS实现AST抽象语法树问题

    前端中的AST抽象语法树问题 四则运算 正则表达式 词法分析 语法分析 完整代码 github地址: https://github.com/feddiyao/Frontend-05-Template/ ...

  4. [译]编写优雅的JavaScript代码 - 最佳实践

    [原文]: devinduct.com/blogpost/22- 有没有似曾相识 如果你对于代码,除了关注是否能准确的执行业务逻辑,还关心代码本身是怎么写的,是否易读,那么你应该会关注如何写出干净优雅 ...

  5. AST(抽象语法树)实战入门:js逆向中滑块加密if语句转化

    概述:AST 抽象语法树 实战 入门 案例 js逆向 js滑块 js加密 极验 瑞数 阿里滑块 5秒盾 ​引言: AST算得上是高端技能.如果把爬虫技能分为初中高三个阶段的话.常规的JS逆向找找参数, ...

  6. JavaScript 设计模式核⼼原理与应⽤实践之单例模式——Vuex的数据管理哲学

    JavaScript 设计模式核⼼原理与应⽤实践之单例模式--Vuex的数据管理哲学 保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式. 单例模式的实现思路 思考这样一个 ...

  7. JavaScript 设计模式核⼼原理与应⽤实践 之 结构型设计模式

    JavaScript 设计模式核⼼原理与应⽤实践 之 结构型设计模式 结构型:装饰器模式--对象装上它,就像开了挂 装饰器模式,又名装饰者模式.它的定义是"在不改变原对象的基础上,通过对其进 ...

  8. AST(抽象语法树)超详细

    自己研究的东西会用到AST,就自己通过查阅资料,整理一下. 本文目录 第一部分:AST的作用 第二部分:AST的流程 第三部分: Eclipse AST的获取与访问 第一部分:AST的作用 首先来一个 ...

  9. 抽象语法树 -Abstract Syntax Tree

    什么是抽象语法树? 是源代码结构的一种抽象表示,以树状的形式表现编程语言的语法结构.树上的每个节点都表示源代码中的一种结构. 拆分成语法树 拆解一个简单的add函数 function add(a, b ...

最新文章

  1. centos7grub配置文件及排错
  2. 【指标统计】删除错误遥信
  3. re匹配正则字符串中的起始和结束元字符的使用方法
  4. 科大星云诗社动态20210120
  5. 蛋白质组学和代谢组学方法在生物标志物发现中的应用 Proteomic and Metabolomic Approaches to Biomarker Discovery
  6. Comet OJ(Contest #8)-C符文能量【dp】
  7. angular 注入器配置_Angular依赖注入介绍
  8. ffplay分析 (视频从Frame(解码后)队列取数据到SDL输出)
  9. 第二十三期:大规模网站架构?你是否熟悉?
  10. kotlin中既继承又实现_Kotlin程序| 解决继承中的主要冲突的示例
  11. visual studio 判断dropdownlist选的是什么_心理测试:五个小蓝人,你选哪个?测你是不是一个容易追求的人...
  12. opendrive道路标准基础知识
  13. 2019年末,10 位院士对 AI 的深度把脉(下)
  14. SecureCRT 中如何配置颜色
  15. 分享几个好用的WP插件,让你的网站牛逼起来
  16. 算法笔记胡凡 7.3.4 连接各点时代码有误
  17. uniapp App端 实现pdf文件预览
  18. 卸载亚信的安全杀毒软件
  19. python生成春联图片,并包装为GUI工具
  20. P2404 自然数的拆分问题(洛谷)

热门文章

  1. 微博html5版登录,新浪微博模拟登录 支持手动处理验证码
  2. CTFHUB——RCE之eval执行和文件包含漏洞
  3. vue + iview
  4. 纯CSS实现粉红爱心动画
  5. Ocelot一个优秀的.NET API网关框架
  6. [Unity3D] MoveTowards、Lerp、Slerp
  7. 凡人修真3D(4)神装
  8. 糖尿病视网膜病变风险随患者年龄和患病持续时间增长
  9. 程序员能纯靠技术渡过中年危机吗
  10. php正则数字是否靓号,数字靓号正则表达式