面试大厂,其中有那么一个问题:
了解过抽象语法树,又称AST,有学习过,也写过一个基于AST的乞丐版模板引擎,先是词法解析token,然后生产抽象语法树,然后更改抽象语法树,当然这是插件做的事情,最后根据新的AST生成代码。
没有,只是看过相关文档
啊,这,我试试!
遂卒....
开玩笑的,既然提到了,又没回答上来什么,哎哟我这暴脾气,一想到今晚就睡不着,连夜把它撸了。
那么我们来从零写个插件吧。
写一个预计算简单表达式的插件
Before:
const result = 1 + 2 + 3 + 4 + 5;
After:
const result = 15;
以上的例子可能大家不会经常遇到,因为傻x才会这么写,但是有可能你会这么写
setTimeout(function(){
// do something
}, 1000 * 2) // 插件要做的事,就是把 1000 * 2 替换成 2000
在写代码之前,你需要明白Babel它的原理,简单点说:Babel解析成AST,然后插件更改AST,最后由Babel输出代码。
那么Babel的插件模块需要你暴露一个function,function内返回visitor。
module.export = function(babel){
return {
visitor:{
}
}
}
visitor是对各类型的AST节点做处理的地方,那么我们怎么知道Babel生成了的AST有哪些节点呢?
很简单,你可以把Babel转换的结果打印出来,或者这里有传送门: AST explorer:https://astexplorer.net/
这里我们看到 const result = 1 + 2中的1 + 1是一个BinaryExpression节点,那么在visitor中,我们就处理这个节点
var babel = require('babel-core');
var t = require('babel-types');
const visitor = {
BinaryExpression(path) {
const node = path.node;
let result;
// 判断表达式两边,是否都是数字
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
// 根据不同的操作符作运算
switch (node.operator) {
case "+":
result = node.left.value + node.right.value;
break
case "-":
result = node.left.value - node.right.value;
break;
case "*":
result = node.left.value * node.right.value;
break;
case "/":
result = node.left.value / node.right.value;
break;
case "**":
let i = node.right.value;
while (--i) {
result = result || node.left.value;
result = result * node.left.value;
}
break;
default:
}
}
// 如果上面的运算有结果的话
if (result !== undefined) {
// 把表达式节点替换成number字面量
path.replaceWith(t.numericLiteral(result));
}
}
};
module.exports = function (babel) {
return {
visitor
};
}
插件写好了,我们运行下插件试试
const babel = require("babel-core");
const result = babel.transform("const result = 1 + 2;",{
plugins:[
require("./index")
]
});
console.log(result.code); // const result = 3;
与预期一致,那么转换 const result = 1 + 2 + 3 + 4 + 5;呢?
结果是: const result = 3 + 3 + 4 + 5;
这就奇怪了,为什么只计算了1 + 2之后,就没有继续往下运算了?
我们看一下这个表达式的AST树
你会发现Babel解析成表达式里面再嵌套表达式。
表达式( 表达式( 表达式( 表达式(1 + 2) + 3) + 4) + 5)
而我们的判断条件并不符合所有的,只符合1 + 2
// 判断表达式两边,是否都是数字
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {}
那么我们得改一改
第一次计算1 + 2之后,我们会得到这样的表达式
表达式( 表达式( 表达式(3 + 3) + 4) + 5)
其中 3 + 3又符合了我们的条件, 我们通过向上递归的方式遍历父级节点 又转换成这样:
表达式( 表达式(6 + 4) + 5)
表达式(10 + 5)
15
// 如果上面的运算有结果的话
if (result !== undefined) {
// 把表达式节点替换成number字面量
path.replaceWith(t.numericLiteral(result));
let parentPath = path.parentPath;
// 向上遍历父级节点
parentPath && visitor.BinaryExpression.call(this, parentPath);
}
到这里,我们就得出了结果 const result = 15;
那么其他运算呢:
const result = 100 + 10 - 50 >>> const result = 60;
const result = (100 / 2) + 50 >>> const result = 100;
const result = (((100 / 2) + 50 * 2) / 50) ** 2 >>> const result = 9;
到这里,已经向你大概的讲解了,如何编写一个Babel插件,再也不怕面试官问我答不出什么了哈...
你以为这就完了吗?
并没有
如果转换这样呢: const result = 0.1 + 0.2;
预期肯定是0.3, 但是实际上,Javascript有浮点计算误差,得出的结果是0.30000000000000004
那是不是这个插件就没卵用?
这就需要你去矫正浮点运算误差了,可以使用Big.js; 比如: result = node.left.value + node.right.value; 改成 result = +new Big(node.left.value).plus(node.right.value);你以为完了吗? 这个插件还可以做很多
比如: Math.PI * 2 >>> 6.283185307179586
比如: Math.pow(2, 2) >>> 4
...
...
有旁友指出:
parentPath那一块可以换个实现方法。第一个binaryExpression真正被计算之后是会被替换成numericLiteral的。由于每个node都会被visit两次,所以在exit访问时,对于父节点而言两个子节点又同时是numericLiteral了,再次执行即可。
也就是说,只要在enter和exit节点的时候,都执行一下上面替换节点的代码就行,无须手动遍历父节点去计算并替换~
BinaryExpression: {
exit: path => {
const node = path.node;
const result = node.left.value + node.right.value
path.replaceWith(t.numericLiteral(result));
}
}
真是个不错的方法呢!
来作者:Axetroy4281
https://juejin.cn/post/6844903566809759758
大家知道,将ES6代码编译为ES5时,我们常用到Babel这个编译工具。大家参考一些网上的文章或者官方文档,里面常会建议大家在.babelrc中输入如下代码
文的babel使用场景局限于babel配合webpack来转译输出es5的js代码,babel的命令行、以代码形式调用或node环境下这些统统都不会涉及。Babel使用的难点主要在于理解polyfill、runtime和core-js。
自从 Babel 由版本5升级到版本6后,在安装和使用方式上与之前大相径庭,于是写了这篇入坑须知,以免被新版本所坑。坑一本地安装和全局安装 、坑二编译插件、坑三babel-polyfill插件
babel-preset-env 一个帮你配置babel的preset,根据配置的目标环境自动采用需要的babel插件。babel-preset-env 功能类似 babel-preset-latest,优点是它会根据目标环境选择不支持的新特性来转译
本文主要内容包括:什么是babel-polyfill,如何使用,如何通过按需加载进行性能优化。babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,此时需要引入babel-polyfill来模拟实现这些对象、方法。
由于现在前端出现了很多非es5的语法,如jsx,.vue,ts等等的格式和写法。babel其实是一个解释器,它主要讲进行中的代码分为三个阶段执行:解释,转换,生成。
在 JavaScript 中,没有基本类型,创建的所有东西都是对象。例如,创建一个新字符串,与其他语言不同,在 JavaScript 中,字符串或数字的声明会自动创建一个封装值的对象,并提供不同的方法,甚至可以在基本类型上执行这些方法。
在项目根目录下新建一个配置文件—— webpack.config.js 文件:执行编译打包命令,完成后打开 bundle.js 文件发现 isNull 和 unique 两个函数没有被编译,和 webpack 官方说法一致:webpack 默认支持 ES6 模块语法,要编译 ES6 代码依然需要 babel 编译器。
Babel对代码进行转换,会将JS代码转换为AST抽象语法树(解析),对树进行静态分析(转换),然后再将语法树转换为JS代码(生成)。每一层树被称为节点。每一层节点都会有type属性,用来描述节点的类型。其他属性用来进一步描述节点的类型。
Babel 对于前端开发者来说应该是很熟悉了,日常开发中基本上是离不开它的。我们已经能够熟练地使用 es2015+ 的语法。但是对于浏览器来说,可能和它们还不够熟悉,我们得让浏览器理解它们,这就需要 Babel
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!