深入理解 Babel - 微内核架构与 ECMAScript 标准化|得物技术

原创
09/05 15:48
阅读数 6K

随着浏览器版本的持续更新,浏览器对JavaScript的支持越来越强大,Babel的重要性显得较低了。但Babel的设计思路、背后依赖的ECMAScript标准化思想仍然值得借鉴。

本文涉及的Babel版本主要是V7.16及以下,截至发文时,Babel最新发布的版本是V7.25.6,未出现大版本更新,近2年也进入了稳定迭代期,本文的分析思路基本适用目前的Babel设计。

一、Babel简介

Babel是什么

Babel是JavaScript转译器,通过Babel,开发者可以自由使用下一代ECMAScript 语法。高版本ECMAScript语法将被转译为低版本语法,以便顺利运行在各类环境,如低版本浏览器、低版本 Node.js 等。

Babel 是转译器,不是编译器。下面是转译和编译的区别:

编译,一般指将一种语言转换为另一种语法和抽象程度等都不同的语言,常见的比如 gcc 编译器。

转译,一般指将一种语言转换为不同版本或者抽象程度相同的语言,比如 Babel 可以把 ECMAScript 6 语法转译为 ECMAScript 5语法。

利用 Babel,开发者可以使用 ECMAScript 的各种新特性进行开发,同时花极少的精力关注浏览器或其他JS运行环境对新特性的支持。甚至,开发者可以根据自身需要,创造属于自己的 JavaScript 语法。

Babel在转译的时候,会对源码进行以下处理: 语法转译(Syntax)和添加API Polyfill。

01.jpg

  • 语法(Syntax)部分 Babel 支持识别高版本语法,并通过插件将源码从高版本语法转译为低版本语法,如:

    • 箭头函数 () => {} 转为普通函数 function() {}。

    • const / let 转译为var

  • API Polyfill

有些运行时相关的 API,语法转译无法解决它们对低版本浏览器等环境的兼容性问题,因此 Babel 通过与 core-js 等工具的配合,实现 API 部分对目标环境(通常是低版本浏览器等)的兼容。

例如[1, 2, 3].include、Promise等 API,Babel 在处理时,如果目标环节可能不支持原生的 include / Promise 的话,Babel 会在转译结果中嵌入 include / Promise 的自定义实现。

有多种方式可以使用 Babel,如: 命令行(babel-cli、babel-node)、浏览器(babel-standalone)、API 调用(babel-core)、webpack loader(babel-loader)等。

转译过程

和多数转译器相同,Babel 运行的生命周期主要是 3 个阶段: 解析、转换、代码生成。

这个过程涉及抽象语法树:

抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。

AST 是树形对象,以结构化的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

02.jpg

源码字符串需要经转译器生成 AST,转译器有很多种,不同转译器,生成的AST对象格式细节可能有差异,但共同点为: 都是树形对象、该树形对象描述了节点特征、各节点之间的关系(兄弟、父子等)。

以下是 Babel 生命周期的三个过程:

  • 解析(Parsing): Code1 ==> 抽象语法树1 解析过程包括 2 个环节: 词法解析、语法解析,最终生成抽象语法树。 词法解析阶段,代码字符串被解析为 token 令牌数组,数组项是一个对象,包括: 代码字符碎片的值、位置、类型等信息。 token 数组是平铺式的数组,没有嵌套的结构信息,它是为语法解析阶段做准备的。 语法解析阶段,token 令牌数组被解析为结构化的抽放语法树对象(AST)。 babel-parser 完成该阶段的主要功能。

03.jpg

  • 转换(Transformation): AST1 ==> AST2 Babel 生成 AST 后,会对 AST 进行遍历,遍历过程中,各类插件对原 AST 对象进行增删改查等操作,AST 结构被修改。

04.jpg

  • 代码生成(Generation): AST2 ==> Code2 Babel 将修改后的 AST 对象转目标代码字符串。 babel-generator 完成该阶段的主要功能。

05.jpg

二、Babel微内核架构

微内核架构

Babel 采用微内核架构,其内核保留核心功能,其余功能利用外部工具和插件机制实现,也体现了"开放-封闭"的设计原则。

06.jpg

除了微内核设计架构,Babel 的模块设计也可以做如下分类:

07.jpg

转译模块

转译模块位于 Babel 微内核架构的"微内核"部分,该部分主要负责代码转译,也就是上面提到的"解析-转换-代码生成"过程。

该模块主要包括: babel-parser、babel-traverse、babel-generator。

  • babel-parser 负责将代码字符串转为 AST 对象。 有 2 点值得一提:

    • babel-parser 本身并不会对 AST 做转换操作,只是负责解析出 AST。AST 转换部分交由各类 plugins 和 presets 处理。

    • babel-parser 内置了对 ESNext/TypeScript/JSX/Flow 最新版本语法的支持,但很多默认是不开启的,目前没有开放插件机制扩展新语法。

  • babel-traverse 在转译过程中,babel-traverse 负责遍历 AST 节点,并根据配置的 plugins/presets,在遍历过程中,对各个 AST 节点进行增删改查的操作。 AST 是一个树形对象,遍历 AST 对象的过程也是一个深度优先遍历的过程。

  • babel-generator 负责将 AST 节点,转为代码字符串,同时也可以生成 source-map。

插件模块

插件模块包括 plugins、presets。

  • plugins 丰富的插件,帮助 Babel 成为一个非常成功的转译工具。 对 AST 的遍历、转换是 Babel 转译的核心功能,但 Babel 本身并不参与该过程,将这些功能作为插件引入到运行时。 具体来说,babel-core 作为核心工具,不提供对 AST 的修改逻辑,通过调用各类插件,实现对 AST 的修改。 Babel的插件分为语法插件和转换插件。

    • 语法插件 值得注意的是,babel-parser 负责将 JavaScript 代码解析出抽象语法树(AST),它支持全面识别 ESNext/TypeScript/JSX/Flow 等语法,目前由 Babel 团队开发维护,不支持插件化。 Babel 插件生态中的语法插件,其功能就是作为"开关",配置是否开启 babel-parser 的某些语法转译功能。 语法插件在 Babel 源码中,以 babel-plugin-syntax 开头。 举个例子:

      • babel-plugin-syntax-decorators 负责开启 babel-parser 对装饰器的语法支持。

      • babel-plugin-syntax-dynamic-import 负责开启 babel-parser 对 import 语句的语法支持。

      • babel-plugin-syntax-jsx 负责开启 babel-parser 对 jsx 语法的支持。

    • 转换插件 转换插件就是社区里常说的 Babel 插件,负责转换 AST 节点。 在介绍babel-traverse时提到,它负责遍历AST对象,每个AST节点会被访问到,等待转换,转换的部分,由"转换插件"负责。 转换插件会提供一个叫做"Visitor"的对象,该对象的 Key 为节点名称,Value 部分提供进入该节点时、离开该节点时的回调函数,在回调函数里,可以对该节点进行一系列操作。 "Visitor" 又称为 "访问者"。

      // plugin 提供 visitor,在 visitor 中对 AST 节点操作 const visitor = { Program: { enter() {}, exit() {}, },

      CallExpression: {
          enter() {},
          exit() {},
      },
      
      NumberLiteral: {
          enter() {},
          exit() {},
      }
      

      }; traverse(ast, visitor);

转换插件在Babel源码中,以 babel-plugin-transform 开头。

举个例子:

  • babel-plugin-transform-strict-mode

该插件拦截 Program 节点,也就是整个程序的根节点,添加 "use strict"指令。

visitor 节点值为函数时,是 enter 回调的快捷方式。

{
    name: "transform-strict-mode",

    visitor: {
      Program(path) {
        const { node } = path;

        for (const directive of node.directives) {
          if (directive.value.value === "use strict") return;
        }

        path.unshiftContainer(
          "directives",
          t.directive(t.directiveLiteral("use strict")),
        );
      },
    },
  };
}
  • babel-plugin-transform-object-assign

该插件负责拦截函数调用表达式节点 CallExpression,将 Object.assign 转为 extends 写法。

{
    name: "transform-object-assign",

    visitor: {
      CallExpression(path, file) {
        if (path.get("callee").matchesPattern("Object.assign")) {
          path.node.callee = file.addHelper("extends");
        }
      },
    },
}
  • Presets

Babel 插件的功能是细粒度的,大部分插件承担了单一功能。

而在实际开发过程中,往往需要支持对各类语法的支持。此时,有两种做法:

A. 需要支持哪些特性,就分别引入支持该特性的插件

B. 直接引入一个插件集合,涵盖所需的各类插件功能

很显然,第一种做法是相对麻烦的。针对第二种做法,Babel提供了插件集 preset。

preset 在 Babel 源码中,以 babel-preset 开头。

例如,Babel 已经提供了几种常用的 preset 供开发者使用:

  1. babel-preset-env
  2. babel-preset-react
  3. babel-preset-flow
  4. babel-preset-typescript
  • 插件运行顺序 Babel 配置项中,plugins 和 presets 均以数组的形式配置,执行时有先后顺序。

    • plugins 在 presets之前运行

    • plugins 按照数组正序执行

    • presets 按照数组倒序执行

08.jpg

工具模块

工具模块提供 Babel 相关模块所需的各类工具,以下一一简要介绍:

  • babel-core babel-core 对外提供了 Babel 多项功能的 API,如转译文件、转译代码、创建/获取配置等,在 Babel 官方文档介绍的比较详细,不再赘述。 值得注意的是,转译类的 API 均提供了同步和异步版本,如 transformSync/transfomAsync、parseSync/parseAsync。

  • babel-cli Babel 的命令行工具,可以直接转译文件夹/文件,它也提供了很多配置项做其他工作,官方文档介绍的比较详细,感兴趣的同学可以去 Babel 官网查看详细配置。

  • babel-standalone Babel 对外服务的很多包是基于 node 环境下使用的,babel-standalone 提供了浏览器下转译的方案。 babel-standalone 内置了所有 Babel 插件,所以体积还是比较大的,而且在浏览器端转译需要时间,比较适合开发、学习使用,不适合在生产环境使用。

举个例子:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>test babel-standalone</title>
    <script src="static/js/babel.min.js"></script>
    <script type="text/babel">
      const arr = [1, 2, 3];
      console.log(...arr);
</script>
  </head>
  <body></body>
</html>

在浏览器运行该 html,可以看到,页面结构变成了:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>test babel-standalone</title>
    <script src="static/js/babel.min.js"></script>
    <script type="text/babel">
      const arr = [1, 2, 3];
      console.log(...arr);
</script>
    <script>
      "use strict";
      var _console;
      var arr = [1, 2, 3];
      (_console = console).log.apply(_console, arr); //# sourceMappingURL=data:application/json;charset=utf-8;base64...
</script>
  </head>
  <body></body>
</html>
  • babel-node 提供在命令行执行高级语法的环境。

例如:

// index.js 里可以使用高级语法     
babel-node -e index.js

index.js 文件以及被其引入的其他文件均可以使用高级语法了。和 babel-cli 不同的是,babel-cli 只负责转换,不在 node 运行时执行;babel-node 会在 node 运行时执行转换,不适合生产环境使用。

  • babel-register

在源文件中,引入babel-register,如 index.js:

index.js

require('babel-register');     
require('./run');

run.js

import fs from 'fs';     
console.log(fs);

执行 node index 时,run.js 就不需要被转码了。

babel-register 通过拦截 node require 方法,为 node 运行时引入了 Babel 的转译能力。

  • babel-loader

babel-loader 是利用 babel-core 的 API 封装的 webpack loader,用于 webpack 构建过程。

  • babel-types

babel-types 是一个非常强大的工具集合,它集成了节点校验、增删改查等功能,是 Babel 核心模块开发、插件开发等场景下不可或缺的工具。

例如:

const t = require('@babel/types');
const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2));
  • babel-template

模板引擎,负责将代码字符串转为 AST 节点对象。

    import { smart as template } from '@babel/template';
    import generate from '@babel/generator';
    import * as t from '@babel/types';

    const buildRequire = template(      var %%importName%% = require(%%source%%);    );

    const ast = buildRequire({
        importName: t.identifier('myModule'),
        source: t.stringLiteral("my-module"),
    });

    const code = generate(ast).code

    console.log(code)

运行结果:

var myModule = require("my-module");
  • babel-code-frame

负责打印出错的代码位置,例如:

const { codeFrameColumns } = require('@babel/code-frame');

const testCode = `
class Run {
    constructor() {}
}
`;

const location = {
    start: {
        line: 2,
        column: 2,
    }
};

const result = codeFrameColumns(testCode, location);

console.log(result);

  1 | class Run {
> 2 |     constructor() {}
    |  ^
  3 | }
  4 |
  • babel-highlight

向控制台输出有颜色的代码片段。该工具可以识别 JavaScript 中的操作符号、标识符、保留字等类型的词法单元,并在终端环境下显示不同的颜色。

运行时相关模块

Babel 配合其插件可以对静态代码进行转译,但有一些遗漏点:

  • 对于运行时涉及的一些高版本 API,并没有提供兼容目标环境的 Polyfill。

  • 转译产物代码可能有些臃肿。

为此,运行时模块(runtime)关注的是转译产物的运行时环境,对运行时提供 API polyfill、代码优化等,该模块涉及几个子包:

  • babel-preset-env

  • babel-plugin-transform-runtime

  • babel-runtime

  • babel-runtime-corejs2/3

  • core-js

接下来以案例解释 runtime 模块的作用。

源码文件 index.js 的内容:

const a = 1; // const 为语法部分
class Base {} // class 为语法部分
new Promise() // Promise 为 API 部分

这段源码包含了语法和 API 部分:

  • const、class 为语法部分

  • Promise 为 API 部分

如果希望这段源码转为 ES5 版本,使构建产物可以运行在不支持 ES6 和 Promise 的环境里,该怎么做呢?

用 babel 命令行执行转译,其中源文件为 index.js,转译产物文件为 index-compiled.js。

npx babel index.js --out-file index-compiled.js

需要配置.babelrc 帮助 Babel 完成语法和 API 部分的转译:

.babelrc:

{
    "presets": [
        [ 
            "@babel/preset-env"
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 3
            }
        ]
    ]
}

简要解释下该配置的原理:

  • babel-preset-env 可以完成语法部分转译,即 const 转译为var 但构建产物中,有些辅助代码如 _classCallCheck 是以硬编码的形式直接写入转译产物的:

    "use strict";

      function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    
      var a = 1;
    
      var Base = function Base() {
          _classCallCheck(this, Base);
      };
    
      new Promise();
    

这样的后果就是构建产物比较臃肿。

  • babel-plugin-transform-runtime 可以将上述 _classCallCheck 置于通用包中,以引用的形式写入转译产物:

    "use strict";

      var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
    
      var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
    
      var a = 1;
    
      var Base = function Base() {
          (0, _classCallCheck2["default"])(this, Base);
      };
    
      new Promise();
    
  • babel-plugin-transform-runtime 的配置参数 corejs 用于转译 API 部分,如 Promsie

      "use strict";
    
      var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
    
      var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
    
      var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
    
      var a = 1;
    
      var Base = function Base() {
          (0, _classCallCheck2["default"])(this, Base);
      };
    
      new _promise"default";
    

Babel 转译过程的运行时优化是一个繁琐的过程,为此将单独用一章讲解运行时优化,感兴趣的同学可以直接阅读 "Babel Runtime" 章节详细了解。

三、标准化

Babel 生态涉及的一些标准化组织。无论是 JavaScript、HTML、DOM、URL 等领域,均需要统一的标准,才能在不同的运行环境下有统一的表现。Babel 转译也需要遵循这些标准,包括 ECMAScript、web标准等。

ECMAScript

JavaScript诞生

1995 年,JavaScript 的第一个版本发布。用时间线的方式描述 JavaScript 的诞生过程会更清晰:

09.jpg

ECMAScript发布

1996 年,微软模仿 JavaScript 实现了 JScript 并内置在 IE3.0,随后,Netscape 公司着手推动 JavaScript 标准化。

这里涉及几个组织:

  • Ecma International Ecma International 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。 Ecma International 的任务包括与有关组织合作开发通信技术和消费电子标准、鼓励准确的标准落实、和标准文件与相关技术报告的出版。 Ecma International 负责多个国际标准的制定:

    • CD-ROM 格式(之后被国际标准化组织批准为ISO 9660)

    • C# 语言规范

    • C++/CLI 语言规范

    • 通用语言架构(CLI)

    • Eiffel 语言

    • 电子产品环境化设计要素

    • Universal 3D 标准

    • OOXML

    • Dart 语言规范

    • ECMAScript 语言规范(以 JavaScript 为基础)ECMA-262 其中就包括 JavaScript 标准语言规范 ECMAScript。 cma International 拥有 ECMAScript 的商标。

  • ECMA TC39 「TC39」全称「Technical Committee 39」译为「第 39 号技术委员会」,是 Ecma International 组织架构中的一部分。 TC39 负责迭代和发展 ECMAScript,它的成员由各个主流浏览器厂商的代表组成,通常每年召开约 6 次会议来讨论未决提案的进展情况,会议的每一项决议必须得到大部分人的赞同,并且没有人强烈反对才可以通过。 TC39 负责:

    • 维护和更新 ECMAScript 语言标准

    • 识别、开发、维护 ECMAScript 的扩展功能库

    • 开发测试套件

    • 为 ISO/IEC JTC 1 提供标准

    • 评估和考虑新添加的标准

  • ISO

国际标准化组织(英语: International Organization for Standardization,简称: ISO)成立于 1947 年 2 月 23 日,制定全世界工商业国际标准的国际标准建立机构。

ISO 的国际标准以数字表示,例如: "ISO 11180:1993" 的 "11180" 是标准号码,而 "1993" 是出版年份。

ISO/IEC JTC 1 是国际标准化组织和国际电工委员会联合技术委员会。其目的是开发、维护和促进信息技术以及信息和通信技术领域的标准。JTC 1 负责了许多关键的 IT 标准,从 MPEG 视频格式到 C++ 编程语言。

10.jpg

  • ECMAScript 发展过程中的关键节点

11.jpg

ECMAScript 各版本

ECMAScript 经历了多个版本,每个版本有自己的特点,简单列举如下:

12.jpg

13.jpg

ECMAScript 迭代过程

一个 ECMAScript 标准的制作过程,包含了 Stage 0 到 Stage 4 共 5 个阶段,每个阶段提交至下一阶段都需要 TC39 审批通过。

14.jpg

15.jpg

特性进入 Stage-4 后,才有可能被加入标准中,还需要 ECMA General Assembly 表决通过才能进入下一次的 ECMAScript 标准中。

如何阅读 ECMAScript

ECMAScript 文档结构

ECMAScript 的规格,可以在 ECMA 国际标准组织的官方网站免费下载和在线阅读。

查看ECMAScript 不同版本的地址:++https://ecma-international.org/publications-and-standards/standards/ecma-262/++

截至 2023年底,已发布的版本如下:

  • ECMA-262 5.1 edition, June 2011

https://262.ecma-international.org/5.1/index.html)

  • ECMA-262, 6th edition, June 2015

https://262.ecma-international.org/6.0/index.html)

  • ECMA-262, 7th edition, June 2016

https://262.ecma-international.org/7.0/index.html)

  • ECMA-262, 8th edition, June 2017

https://262.ecma-international.org/8.0/index.html)

  • ECMA-262, 9th edition, June 2018

https://262.ecma-international.org/9.0/index.html)

  • ECMA-262, 10th edition, June 2019

https://262.ecma-international.org/10.0/index.html)

  • ECMA-262, 11th edition, June 2020

https://262.ecma-international.org/11.0/index.html)

  • ECMA-262, 12th edition, June 2021

https://262.ecma-international.org/12.0/index.html)

  • ECMA-262, 13th edition, June 2022

https://262.ecma-international.org/13.0/index.html)

  • ECMA-262, 14th edition, June 2023

https://262.ecma-international.org/14.0/index.html)

每个版本有独立的网址,格式为: https://262.ecma-international.org/{version}/,比如 ECMAScript 14.0 版本的网址为 https://262.ecma-international.org/14.0/。

从章节数量上,ECMAScript 6.0、ECMAScript 7.0 有 26 章,之后的版本有 27-29 章,虽然章节数量不同,规格章节的分布是保持一定规律的,以 ECMAScript 11.0 版本为例:

  • Introduction: 介绍部分

该章节简要描述了: JavaScript 和 ECMAScript 的发展历史、不同 ECMAScript 规格的主要更新内容。

  • 第 1 章到第 3 章: 描述了规格文件本身,而非语言

    • 第 1 章用一句话描述了该规格的描述范围。

    • 第 2 章描述了基于规格的"实现"的一致性要求:

      • "实现"必须支持规格中描述的所有类型、值、对象、属性、函数以及程序的语法和语义

      • "实现"必须按照 Unicode 标准和 ISO/IEC 10646 的最新版本处理文本输入

      • "实现"如果提供了应用程序编程接口(API),那么该 API 需要适应不同的人文语言和国家差异,且必须实现最新版本的 ECMA-402 所定义的与本规范相兼容的接口

      • "实现"可以支持该规格中没有提及的类型、值、对象、属性、函数、正则表达式语法以及其他编程写法

      • "实现"不能实现该规格中禁止的写法

  • 第 3 章描述了该规格的一些参考资料:

    • ISO/IEC 10646

    • ECMA-402

    • EMCA-404 JSON 数据交换格式规范

  • 第 4 章: 对这门语言总体设计的描述。

  • 第 5 章到第 8 章: 语言宏观层面的描述。

  • 第 6 章介绍数据类型。

  • 第 7 章介绍语言内部用到的抽象操作。

  • 第 8 章介绍代码如何运行。

  • 第 9 章到第 27 章: 介绍具体的语法。

一般而言,除非写编译器,开发者无需阅读 ECMAScript 的规格,规格的内容非常多,如无必要也无需通读。只是在遇到一些奇怪的问题时,阅读官方规格,是最稳妥的办法。

通过阅读规格解决一些问题(以ECMAScript 11.0为例)

  • 识别关键词和保留字,并高亮 Babel 工具集中的 babel-highlight,可以实现在终端对代码块中的目标字符单元显示不同的颜色。这里需要识别不同字符单元的类型,如关键字、保留字、标识符、数字、字符串等。 标识符、数字、字符串都很好理解和识别,但哪些字符应该被识别为关键字、保留字,而不是标识符呢? 此时可以阅读 ECMAScript 规格了,ECMAScript 11.0 规格的 11.6.2 节介绍了关键词和保留字列表。

    • 关键词(keywords) 关键词首先是标识符,同时有语义,包括 if、while、async、await...,个别关键词是可以用作变量名的。

    • 保留字(reserved word) 保留字首先是标识符,但不能用作变量名。 部分关键词是保留字,但部分不是: if、while是保留字;await只有在 async方法和模块中才是保留字;async不是保留字,它可以作为普通的变量名使用。

    • 保留字列表

      await break case catch class const continue window default delete do else enum export extends false finally for function if import in instanceof new null returns uper switch this throw true try typeof var void while with yield

读完上述规格,也就知道哪些字符单元是需要识别为保留字与关键词,并高亮的了。

  • 识别全局对象,并高亮 继续使用 babel-highlight 实现代码块中的全局对象高亮,那么,我们需要知道哪些是规格中描述的全局变量。 规格的 18 章介绍了全局对象,通过该章的描述,可以知道:

    • 全局属性 全局属性有: globalThis、Infinity、NaN、undefined。

      • 全局方法 全局方法有: eval(x)、isFinite、isNaN、parseFloat、parseInt、decodeURIComponent、encodeURIComponent 等。

      • 全局构造函数 全局的构造函数有:

      Array ArrayBuffer BigInt BigInt64Array BigUnit64Array Boolean DataView Date Error EvalError Float32Array Float64Array Function Int8Array Int16Array Int32Array Map Number Object Promise Proxy RangeError ReferenceError RegExp Set SharedArrayBuffer String Symbol SyntaxError TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array URIError WeakMap WeakSet

  • 其他的全局属性 Atomics、JSON、Math、Reflect。 很显然,当字符单元的名称是上述名称中的一员时,我们可以对其进行高亮处理了(若上下文中无重新定义的同名变量)。

  • 自定义 Error babel-loader 自身维护了私有的 LoaderError 对象,该对象继承自原生 Error 类,并且订制了部分实例属性。代码如下:

    class LoaderError extends Error { constructor(err) { super();

          const { name, message, codeFrame, hideStack } = format(err);
    
          this.name = "BabelLoaderError";
    
          this.message = ${name ? ${name}: ` : ""}${message}\n\n${codeFrame}\n`;
    
          this.hideStack = hideStack;
    
          Error.captureStackTrace(this, this.constructor);
      }
    

    }

可以看到,babel-loader 自定义了错误实例的 name、message、hideStack 属性,那么,问题是,原生的 Error 类有哪些属性和方法,哪些是开发者可以自定义的呢?

规格的 19.5 章节,详细介绍了 Error 的各类规范:

  • Error 作为函数被调用时(Error(...)),表现和 new Error(...) 一致,均会创建并返回 Error 的新实例

  • Error 可以被继承,比如通过 extends 的方式,子类必须提供 constructor 方法,且该方法内必须提供 super() 调用

  • Error 构造函数必须有 prototype 属性

    • Error.prototype 属性需有以下属性:

      • Error.prototype.constructor: 指向构造函数

      • Error.prototype.message: 描述错误信息,默认是空字符串

      • Error.prototype.name: 描述错误名称,默认值是 Error

从 LoaderError 的源码可以看到,LoaderError 做了以下几件事情:

  • LoaderError 继承自 Error

  • 实例自定义了 name、message 属性,明确 babel-loader 的信息

  • 实例自定义的 hideStack 属性是非标准属性,用于 babel-loader 内部

web标准

是在解决 API Polyfil 的时候,Babel 配合使用的 core-js 除了提供 ECMAScript 标准下的 JavaScript API 实现,也提供了 DOM/URL 等实现。而 DOM/URL 所属的 web 标准,由 W3C/WHATWG 制定。

16.jpg

经过多年发展,WHATWG 和 W3C 目前是合作关系,其中,WHATWG 维护 HTML 和 DOM 标准,W3C 使用 WHATWG 存储库中的 HTML 和 DOM 标准描述,W3C 在 HTML 部分的工作集中在 XHTML/XML 上。

17.jpg

总结

本文介绍了 Babel 的概述/微内核架构/ECMAScript标准化方面的设计思想和部分实现原理。

上述内容其实在很早之前就已经成型了,笔者也查看了Babel最近的迭代内容,发现并没有太大的变化。至于代码转译领域,目前是Babel还是其他工具哪个更有优势,不在本文的讨论范围内。除了比较社区哪些工具更好而言,"Babel的设计思路、其与标准规范是怎么配合的"等也是很值得学习的地方,也是这篇文章的产生背景。

希望本文对你有所帮助!

*文 / hoperyy

本文属得物技术原创,更多精彩文章请看:得物技术

未经得物技术许可严禁转载,否则依法追究法律责任!

展开阅读全文
加载中
点击加入讨论🔥(1) 发布并加入讨论🔥
1 评论
12 收藏
1
分享
返回顶部
顶部