用JS解释JS!详解AST及其应用

一 AST 是什么?

1 AST:Abstract Syntax Tree - 抽象语法树

当我们查看目前主流的项目中的 devDependencies,会发现各种各样的模块工具。归纳一下有:JavaScript转译、css预处理器、elint、pretiier 等等。这些模块我们不会在生产环境用到,但它们在我们的开发过程中充当着重要的角色,而所有的上述工具,都建立在 AST 的基础上。

2 AST 工作流程

parse:把代码解析为AST。

transform:对AST中的各个节点做相关操作,如新增、删除、替换、追加。业务开发 95%的代码都在这里。

generator:把AST转换为代码。

3 AST 树预览

AST 辅助开发工具:https://astexplorer.net/

二 从一个简单需求上手

代码压缩的伪需求:将 square 函数参数与引用进行简化,变量由 num 转换为 n:

解法1:使用 replace 暴力转换

const sourceText = `functionsquare(num){return num * num;}`;

sourceText.replace(/num/g, 'n');

以上操作相当的暴力,很容易引起bug,不能投入使用。如若存在字符串 "num",也将被转换:

// 转换前functionsquare(num) {return num * num;}console.log('param 2 result num is ' + square(2));

// 转换后functionsquare(n) {return n * n;}console.log('param 2 result n is ' + square(2));

解法2:使用 babel 进行 AST 操作

module.exports = () => {return {visitor: {// 定义 visitor, 遍历 Identifier Identifier(path) {if (path.node.name === 'num') { path.node.name = 'n'; // 转换变量名 } } } }};

通过定义 Identifier visitor,对 Identifier(变量) 进行遍历,如果 Identifier 名称为 "num",进行转换。以上代码解决了 num 为字符串时也进行转换的问题,但还存在潜在问题,如代码为如下情况时,将引发错误:

// 转换前functionsquare(num) {return num * num;}console.log('global num is ' + window.num);

// 转换后functionsquare(n) {return n * n;}console.log('global num is ' + window.n); // 出错了

由于 window.num 也会被上述的 visitor 迭代器匹配到而进行转换,转换后出代码为 window.n,进而引发错误。分析需求"将 square 函数参数与引用进行简化,变量由 num 转换为 n",提炼出的3个关键词为 "square 函数、参数、引用",对此进一步优化代码。

解法2升级:找到引用关系

module.exports = () => {return {visitor: { Identifier(path,) {// 三个前置判断if (path.node.name !== 'num') { // 变量需要为 numreturn; }if (path.parent.type !== 'FunctionDeclaration') { // 父级需要为函数return; }if (path.parent.id.name !== 'square') { // 函数名需要为 squarereturn; }const referencePaths = path.scope.bindings['num'].referencePaths; // 找到对应的引用 referencePaths.forEach(path => path.node.name = 'n'); // 修改引用值 path.node.name = 'n'; // 修改自身的值 }, } }};

上述的代码,可描述流程为:

转换结果:

// 转换前functionsquare(num) {return num * num;}console.log('global num is ' + window.num);

// 转换后functionsquare(n) {return n * n;}console.log('global num is ' + window.num);

在面向业务的AST操作中,要抽象出"人"的判断,做出合理的转换。

三 Babel in AST

1 API 总览

// 三剑客const parser = require('@babel/parser').parse;const traverse = require('@babel/traverse').default;const generate = require('@babel/generator').default;

// 配套包const types = require('@babel/types');

// 模板包const template = require('@babel/template').default;

2 @babel/parser

通过 babel/parser 将源代码转为 AST,简单形象。

const ast = parser(rawSource, {sourceType: 'module',plugins: ["jsx", ],});

3 @babel/traverse

AST 开发的核心,95% 以上的代码量都是通过 @babel/traverse 在写 visitor。

const ast = parse(`functionsquare(num){return num * num;}`);

traverse(ast, { // 进行 ast 转换 Identifier(path) { // 遍历变量的visitor// ... },// 其他的visitor遍历器 } )

visitor 的第一个参数是 path,path 不直接等于 node(节点),path 的属性和重要方法组成如下:

4 @babel/generator

通过 @babel/generator 将操作过的 AST 生成对应源代码,简单形象。

const output = generate(ast, { /* options */ });

5 @babel/types

@babel/types 用于创建 ast 节点,判断 ast 节点,在实际的开发中会经常用到。

// is开头的用于判断节点types.isObjectProperty(node);types.isObjectMethod(node);

// 创建 null 节点const nullNode = types.nullLiteral();// 创建 square 变量节点const squareNode = types.identifier('square');

6 @babel/template

@bable/types 可以创建 ast 节点,但过于繁琐,通过 @babel/template 则可以快速创建整段的 ast 节点。下面对比了获得 import React from 'react' ast 节点的两种方式:

// @babel/types// 创建节点需要查找对应的 API,传参需要匹配方法const types = require('@babel/types');const ast = types.importDeclaration( [ types.importDefaultSpecifier(types.identifier('React')) ], types.stringLiteral('react'));

// path.replaceWith(ast) // 节点替换

// 使用 @babel/template// 创建节点输入源代码即可,清晰易懂const template = require('@babel/template').default;const ast = template.ast(`import React from'react'`);

// path.replaceWith(ast) // 节点替换

7 定义通用的 babel plugin

定义通用的 babel plugin,将有利于被 Webpack 集成,示例如下:

// 定义插件const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) => {return { name: 'your-plugin', // 定义插件名 visitor: { // 编写业务 visitor Identifier(path,) {// ... }, } }});

// 配置 babel.config.jsmodule.exports = {presets: [require('@babel/preset-env'), // 可配合通用的 present ],plugins: [require('your-plugin'),// require('./your-plugin') 也可以为相对目录 ]};

在 babel plugin 开发中,可以说就是在写 ast transform callback,不需要直接接触"@babel/parser、@babel/traverse、@babel/generator"等模块,这在 babel 内部调用了。

在需要用到 @babel/types 能力时,建议直接使用 @babel/core,从源码[1]可以看出,@babel/core 直接透出了上述 babel 模块。

const core = require('@babel/core');const types = core.types; // const types = require('@babel/types');

四 ESLint in AST

在掌握了 AST 核心原理后,自定义 ESlint 规则也变的容易了,直接上代码:

// eslint-plugin-my-eslint-pluginmodule.exports.rules = { "var-length": context => ({ // 定义 var-length 规则,对变量长度进行检测 VariableDeclarator: (node) => { if (node.id.name.length <= 1){ context.report(node, '变量名长度需要大于1'); } } })};

// .eslintrc.jsmodule.exports = { root: true, parserOptions: { ecmaVersion: 6 }, plugins: ["my-eslint-plugin" ], rules: {"my-eslint-plugin/var-length": "warn" }};

体验效果

IDE 正确提示:

执行 eslint 命令的 warning:

查阅更多 ESLint API 可查看官方文档[2]。

五 获得你所需要的 JSX 解释权

第一次接触到 JSX 语法大多是在学习 React 的时候,React 将 JSX 的能力发扬光大[3]。但 JSX 不等于 React,也不是由 React 创造的。

// 使用 react 编写的源码const name = 'John';const element = <div>Hello, {name}</div>;

// 通过 @babel/preset-react 转换后的代码const name = 'John';const element = React.createElement("div", null, "Hello, ", name);

JSX 作为标签语法既不是字符串也不是 HTML,是一个 JavaScript 的语法扩展,可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 会使人联想到模版语言,它也具有 JavaScript 的全部功能。下面我们自己写一个 babel plugin,来获得所需要对 JSX 的解释权。

1 JSX Babel Plugin

2 目标

exportdefault (<view> hello <textstyle={{fontWeight: 'bold' }}>world</text></view>);

<!-- 输出 Web HTML --><div> hello <spanstyle="font-weight: bold;">world</span></div>

// jsx 源码module.exports = function () {return (<viewvisibleonTap={e => console.log('clicked')} >ABC<button>login</button></view> );};

// 目标:转后为更通用的 JavaScript 代码module.exports = function () {return {"type": "view","visible": true,"children": ["ABC", {"type": "button","children": ["login1" ] } ] };};

明确了目标后,我们要做的事为:

1. 将 jsx 标签转为 Object,标签名为 type 属性,如 <view /> 转化为 { type: 'view' }

2. 标签上的属性平移到 Object 的属性上,如 <view onTap={e => {}} /> 转换为 { type: 'view', onTap: e => {} }

3. 将 jsx 内的子元素,移植到 children 属性上,children 属性为数组,如 { type: 'view', style, children: [...] }

4. 面对子元素,重复前面3步的工作。

下面是实现的示例代码:

const { declare } = require('@babel/helper-plugin-utils');const jsx = require('@babel/plugin-syntax-jsx').default;const core = require('@babel/core');const t = core.types;

/* 遍历 JSX 标签,约定 node 为 JSXElement,如 node = <view onTap={e => console.log('clicked')} visible>ABC<button>login</button></view>*/const handleJSXElement = (node) => {const tag = node.openingElement;consttype = tag.name.name; // 获得表情名为 Viewconst propertyes = []; // 储存对象的属性 propertyes.push( // 获得属性 type = 'ABC' t.objectProperty( t.identifier('type'), t.stringLiteral(type) ) );const attributes = tag.attributes || []; // 标签上的属性 attributes.forEach(jsxAttr => { // 遍历标签上的属性switch (jsxAttr.type) {case'JSXAttribute': { // 处理 JSX 属性const key = t.identifier(jsxAttr.name.name); // 得到属性 onTap、visibleconst convertAttributeValue = (node) => {if (t.isJSXExpressionContainer(node)) { // 属性的值为表达式(如函数)return node.expression; // 返回表达式 }// 空值转化为 true, 如将 <view visible /> 转化为 { type: 'view', visible: true }if (node === null) {return t.booleanLiteral(true); }return node; }const value = convertAttributeValue(jsxAttr.value); propertyes.push( // 获得 { type: 'view', onTap: e => console.log('clicked'), visible: true } t.objectProperty(key, value) );break; } } });const children = node.children.map((e) => {switch(e.type) {case'JSXElement': {return handleJSXElement(e); // 如果子元素有 JSX,便利 handleJSXElement 自身 }case'JSXText': {return t.stringLiteral(e.value); // 将字符串转化为字符 } }return e; }); propertyes.push( // 将 JSX 内的子元素转化为对象的 children 属性 t.objectProperty(t.identifier('children'), t.arrayExpression(children)) );const objectNode = t.objectExpression(propertyes); // 转化为 Object Node/* 最终转化为 { "type": "view", "visible": true, "children": [ "ABC", { "type": "button", "children": [ "login" ] } ] } */return objectNode;}

module.exports = declare((api, options) => {return { inherits: jsx, // 继承 Babel 提供的 jsx 解析基础 visitor: { JSXElement(path) { // 遍历 JSX 标签,如:<view />// 将 JSX 标签转化为 Object path.replaceWith(handleJSXElement(path.node)); }, } }});

六 总结

我们介绍了什么是 AST、AST 的工作模式,也体验了利用 AST 所达成的惊艳能力。现在来想想 AST 更多的业务场景是什么?当用户:

需要基于你的基础设施进行二次编程开发的时候

有可视化编程操作的时候

有代码规范定制的时候

AST 将是你强有力的武器。

注:本文演示的代码片段与测试方法在 https://github.com/chvin/learn_ast,有兴趣的读者可前往学习体验。

招聘

参考资料

[1]https://github.com/babel/babel/blob/main/packages/babel-core/src/index.js#L10-L14

[2]https://cn.eslint.org/docs/developer-guide/working-with-rules

[3]https://reactjs.bootcss.com/docs/introducing-jsx.html

技术公开课

《React 入门与实战》

React是一个用于构建用户界面的JavaScript库。本课程共54课时,带你全面深入学习React的基础知识,并通过案例掌握相关应用。

「点点赞赏,手留余香」

赞赏

  • 聻壅必奴
  • 四时景蚊子姐
  • 2人赞过
2
0
0
评论 0 请文明上网,理性发言

相关文章

  • 1、console.log输出console.log(([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]) 2、优雅的取随机字符串Math.random().toString(16).substring(2) 3、if比较["toString"]()==="10" 4、优雅的取整vara=2.3
    Pr83ho0 5 2 0 条评论
  • 在电影《叶问》中,甄子丹饰演的叶问每天会利用木人桩练习武艺,使得泳春拳法成为肌肉记忆的一部分。临场迎敌时,身体会使用正确的拳式,自然而然。 我以为JS编程基本功的练习于武术拳法的练习是一样的。通过一套有效的『拳法』将JS基本功内置其中,反复练习,使得JS技艺成为手指记忆的一部分。这样,当面对实际开发时,我们就不需要为
    tud3yhp8t_ 7 0 0 条评论
  • 前言我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。 这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因
    小小呆萌 7 0 0 条评论
  • 一、是什么函数式编程是一种"编程范式"(programmingparadigm),一种编写程序的方法论 主要的编程范式有三种:命令式编程,声明式编程和函数式编程 相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程
    程语嫣晨芳芳响 8 9 0 条评论
  • 探索函数式编程,通过它让你的程序更具有可读性和易于调试--MattBanz 本文导航?什么不是函数式编程04%?纯函数08%?不变性17%?函数组合42%?递归50%?高阶函数69%?柯里化80%?总结95%编译自|https://opensource.com/article/17/6/functional-java
    韩兔兔 5 3 0 条评论
  • 作者丨AlexanderHafemann 译者|布加迪 用JavaScript编写第一段代码可能需要一天左右的时间,但深入了解其未知知识和背景知识可能让你在整个职业生涯受益无穷! 作用域(scope) 简而言之,作用域就是"你可以在代码中访问声明的地方"。 我们有两种作用域:全局作用域和局部作用域,区别在于你可以在代
    保罗8007 5 0 0 条评论
  • 前面的文章我们一起玩了Flowable中的ServiceTask,今天我们再来看看Flowable中的脚本任务。 1.脚本任务个人感觉脚本任务和我们前面说的ServiceTask很像,都是流程走到这个节点的时候自动做一些事情,不同的是,在ServiceTask中,流程在这个节点中所做的事情是用Java代码写的,在脚本任
    摩羯会弹琴吗 5 0 0 条评论
  • 网络研究院 新的Windows零日漏洞允许威胁参与者使用恶意的独立JavaScript文件绕过Web标记安全警告。已经看到威胁参与者在勒索软件攻击中使用零日漏洞。Windows包含一个称为Web标记(MoTW)的安全功能,该功能将文件标记为已从Internet下载,因此应谨慎处理,因为它可能是恶意的。MoTW标志作为
    隐若蕊 5 0 0 条评论
  • 写在前面 为了提升应用稳定性,我们对前端项目开展了脚本异常治理的工作,对生产上报的jserror进行了整体排查,试图通过降低脚本异常的发生频次来提升相关告警的准确率,结合最近在这方面阅读的相关资料,尝试阶段性的做个总结,下面我们来介绍下js异常处理的一些经验。先说概念 什么是异常先来看一下官方的定义:Errorobj
    莫灬z 7 0 0 条评论
  • 喜讯 在2022年9月,由海南省计算机学会组织的全省全国青少年信息学奥林匹克联赛(NOIP)及CSP-JS非专业级别的能力认证考试,以及2022海南省青少年信息学竞赛中,我校共六名学生参加第一轮认证暨2022海南省青少年信息学竞赛(初赛),并全部入围第二轮认证暨复赛,其中获得二等奖2人,三等奖3人。 向以上获奖的同学表
    乜环可 7 3 0 条评论