使用Babel创建自定义JavaScript语法结构。 第二部分

今天,我们将发布使用Babel进行JavaScript语法扩展的翻译的第二部分。



→头晕目眩

解析方式


解析器从代码令牌化系统接收令牌列表,并一次检查一个令牌,以构建一个AST。 为了决定如何使用标记,并理解接下来可以使用哪个标记,解析器引用了语言语法的规范。

语法规范如下所示:

... ExponentiationExpression -> UnaryExpression                             UpdateExpression ** ExponentiationExpression MultiplicativeExpression -> ExponentiationExpression                             MultiplicativeExpression ("*" or "/" or "%") ExponentiationExpression AdditiveExpression    -> MultiplicativeExpression                             AdditiveExpression + MultiplicativeExpression                             AdditiveExpression - MultiplicativeExpression ... 

它描述了执行表达式或语句的优先级。 例如,一个AdditiveExpression表达式可以代表以下构造之一:

  • 表达MultiplicativeExpression表达。
  • AdditiveExpression表达式,后跟+标记运算符,然后是MultiplicativeExpression表达式。
  • AdditiveExpression表达式,后跟“ - ”标记,然后是MultiplicativeExpression表达式。

结果,如果我们有表达式1 + 2 * 3 ,那么它将看起来像这样:

 (AdditiveExpression "+" 1 (MultiplicativeExpression "*" 2 3)) 

但事实并非如此:

 (MultiplicativeExpression "*" (AdditiveExpression "+" 1 2) 3) 

使用这些规则的程序将转换为解析器发出的代码:

 class Parser {  // ...  parseAdditiveExpression() {    const left = this.parseMultiplicativeExpression();    //    -  `+`  `-`    if (this.match(tt.plus) || this.match(tt.minus)) {      const operator = this.state.type;      //          this.nextToken();      const right = this.parseMultiplicativeExpression();      //        this.finishNode(        {          operator,          left,          right,        },        'BinaryExpression'      );    } else {      //  MultiplicativeExpression      return left;    }  } } 

请注意,这是Babel中实际存在的极其简化的版本。 但是我希望这段代码可以使我们说明正在发生的事情的本质。

如您所见,解析器本质上是递归的。 它从最低优先级设计过渡到最高优先级设计。 例如, parseAdditiveExpression调用parseMultiplicativeExpression ,而此构造调用parseExponentiationExpression依此类推。 此递归过程称为递归下降解析

功能this.eat,this.match,this.next


您可能已经注意到,在前面的示例中,使用了一些辅助功能,例如this.eatthis.matchthis.next等。 这些是Babel解析器的内部功能。 但是,这些功能并非Babel独有;它们通常存在于其他解析器中。

  • this.match函数返回一个布尔值,该布尔值指示当前令牌是否满足指定条件。
  • this.next函数在令牌列表中向前this.next到下一个令牌。
  • this.eat函数返回的内容与this.eat函数相同;如果this.match返回true ,则this.match将在返回true之前执行对this.next的调用。
  • this.lookahead函数使您无需前进即可获取下一个令牌,这有助于对当前节点进行决策。

如果再次查看我们更改的解析器代码,您会发现阅读它变得更加容易:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  parseStatementContent(/* ...*/) {    // ...    // NOTE:   match        if (this.match(tt._function)) {      this.next();      // NOTE:     ,          this.parseFunction();    }  }  // ...  parseFunction(/* ... */) {    // NOTE:   eat         node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);    node.id = this.parseFunctionId();  } } 

我知道我没有深入解释解析器的功能。 因此, 到处都是 -关于此主题的一些有用资源。 我学到了很多,可以向您推荐。

当我展示出现在AST中的新“ curry ”属性时,您可能对学习如何可视化在Babel AST Explorer中创建的语法感兴趣。

由于我在Babel AST Explorer中添加了一项新功能,使您可以将自己的解析器加载到此AST研究工具中,因此这成为可能。

如果沿着路径packages/babel-parser/lib ,您可以找到解析器的编译版本和代码映射。 在Babel AST Explorer面板中,您可以看到用于加载自己的解析器的按钮。 通过下载packages/babel-parser/lib/index.js您可以可视化使用自己的解析器生成的AST。


AST可视化

我们的Babel插件


现在解析器已经完成,让我们为Babel编写一个插件。

但是,也许现在您对我们将如何使用我们自己的Babel解析器有一些疑问,尤其是考虑到我们使用哪种技术堆栈来构建项目。

没错,没有什么可担心的。 Babel插件可以提供解析器功能。 相关文档可在Babel网站上找到。

 babel-plugin-transformation-curry-function.js import customParser from './custom-parser'; export default function ourBabelPlugin() {  return {    parserOverride(code, opts) {      return customParser.parse(code, opts);    },  }; } 

由于我们创建了Babel解析器的分支,这意味着所有现有的解析器功能以及内置插件将继续正常运行。

消除这些疑虑后,让我们看一下如何制作一个支持currying的函数。

如果您不能满足期望并且已经尝试将我们的插件添加到项目构建系统中,则可能会注意到支持currying的功能已编译为常规功能。

发生这种情况的原因是,在解析和转换代码后,Babel使用@babel/generator从转换后的AST生成代码。 由于@babel/generator对新的curry属性一无所知,因此它只会忽略它。

如果某天支持currying的函数进入了JavaScript标准,那么您可能想做一个PR在此处添加新代码。

为了使该函数支持currying,可以将其包装在一个高阶函数currying

 function currying(fn) {  const numParamsRequired = fn.length;  function curryFactory(params) {    return function (...args) {      const newParams = params.concat(args);      if (newParams.length >= numParamsRequired) {        return fn(...newParams);      }      return curryFactory(newParams);    }  }  return curryFactory([]); } 

如果您对JS中的currying函数机制的实现功能感兴趣,请阅读材料。

结果,我们通过转换支持currying的函数可以做到这一点:

 //   function @@ foo(a, b, c) {  return a + b + c; } //    const foo = currying(function foo(a, b, c) {  return a + b + c; }) 

目前,我们将不关注JavaScript中引发函数的机制,该机制使您可以在定义函数之前调用foo

转换代码如下所示:

 babel-plugin-transformation-curry-function.js export default function ourBabelPlugin() {  return {    // ... <i>    visitor: {      FunctionDeclaration(path) {        if (path.get('curry').node) {          // const foo = curry(function () { ... });          path.node.curry = false;          path.replaceWith(            t.variableDeclaration('const', [              t.variableDeclarator(                t.identifier(path.get('id.name').node),                t.callExpression(t.identifier('currying'), [                  t.toExpression(path.node),                ])              ),            ])          );        }      },    },</i>  }; } 

如果您阅读了有关Babel转换的材料,您将更容易弄清楚。

现在,我们面临着如何为该机制提供对currying函数的访问权的问题。 在这里,您可以使用以下两种方法之一。

▍方法1:可以假设在全局范围内声明了currying函数


如果是这样,则该工作已经完成。

如果在执行编译后的代码时发现未定义currying函数,那么我们将遇到一条错误消息,看起来像“ currying is not defined ”。 它与消息“ 未定义regeneratorRuntime ”非常相似。

因此,如果有人使用您的babel-plugin-transformation-curry-function ,则可能需要通知他,他需要安装currying polyfill以确保此插件正常工作。

▍方法2:可以使用babel / helpers


您可以在@babel/helpers添加新的辅助函数。 这种发展不太可能与官方的@babel/helpers结合使用。 结果,您将必须找到一种方法来显示@babel/core @babel/helpers

 package.json {  "resolutions": {    "@babel/helpers": "7.6.0--your-custom-forked-version",  } 

我自己还没有尝试过,但是我相信这种机制会起作用。 如果您尝试它并遇到问题,我将很乐意讨论

@babel/helpers新的helper函数非常简单。

首先,转到packages / babel-helpers / src / helpers.js文件,并添加一个新条目:

 helpers.currying = helper("7.6.0")`  export default function currying(fn) {    const numParamsRequired = fn.length;    function curryFactory(params) {      return function (...args) {        const newParams = params.concat(args);        if (newParams.length >= numParamsRequired) {          return fn(...newParams);        }        return curryFactory(newParams);      }    }    return curryFactory([]);  } `; 

描述辅助功能时,会指出所需的版本@babel/core 。 这里的一些困难可能是由于currying函数的export default引起的。

要使用一个辅助函数,只需调用this.addHelper()

 // ... path.replaceWith(  t.variableDeclaration('const', [    t.variableDeclarator(      t.identifier(path.get('id.name').node),      t.callExpression(this.addHelper("currying"), [        t.toExpression(path.node),      ])    ),  ]) ); 

如有必要, this.addHelper命令将在文件顶部嵌入帮助程序功能,并返回一个Identifier已实现功能的Identifier

注意事项


我参与Babel的研究已经有一段时间了,但是我还没有为解析器添加支持新JavaScript语法的功能。 我主要致力于修复错误和改进与官方语言功能有关的内容。

但是,一段时间以来,我一直沉迷于向该语言添加新的语法构造的想法。 结果,我决定编写有关它的材料并尝试。 令人欣喜的是,它们都能按预期运行。

控制您使用的语言的语法的能力是强大的灵感来源。 通过实现一些复杂的结构,这使得编写比以前更少的代码或编写更简单的代码成为可能。 将简单代码转换为复杂结构的机制是自动化的,并已转移到编译阶段。 这让人想起async/await如何解决地狱回调和承诺长链的问题。

总结


在这里,我们讨论了如何修改Babel解析器的功能,编写了自己的代码转换插件,简要讨论了@babel/generator和使用@babel/helpers创建帮助器函数。 关于代码转换的信息仅示意性给出。 在此处阅读有关它们的更多信息。

在此过程中,我们谈到了解析器的某些功能。 如果您对这个主题感兴趣- 在这里那里 -对您有用的资源。

我们执行的操作序列与将新的JavaScript功能提交给TC39时所执行的过程的一部分非常相似。 这是 TC39存储库页面,您可以在其中找到有关当前报价的信息。 在这里,您可以找到有关如何使用类似优惠的更多详细信息。 当提出一项新的JavaScript功能时,提供此功能的人通常会编写polyfill或通过分叉Babel进行演示以证明该句子有效。 如您所见,创建新的JS功能过程中,创建解析器的分叉或编写polyfill并不是最困难的部分。 很难确定创新的主题领域,难以计划和思考使用创新的方案和边界案例; 很难收集JavaScript程序员社区成员的意见和建议。 因此,我要感谢所有能够提供TC39新JavaScript功能从而开发这种语言的人。

这是 GitHub上页面,可让您大致了解我们在这里所做的事情。

亲爱的读者们! 您是否曾经想扩展JavaScript的语法?


Source: https://habr.com/ru/post/zh-CN470878/


All Articles