【译】ECMAScript 6模块:最终的语法

麻阳
2023-12-01
简介: 2014年7月底,TC39又召开了一次会议,最后敲定了ECMAScript 6 (ES6)模块语法的最后细节。本文概述了完整的ES6模块系统。


1、当前的模块系统

javaScript没有内置对模块的支持,但是社区为此创建了令人满意的变通方法。 而这就要说到下面两条重要的标准:
  • CommonJS Modules :
1、简洁的语法
2、为同步加载而设计的,主要是用于服务器端。规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作

  • Asynchronous Module Definition (AMD):
1、Slightly more complicated syntax, enabling AMD to work without eval() (or a compilation step).(稍微复杂一点的语法,使AMD可以在没有eval()(或编译步骤)的情况下工作。这个没太懂,大概是指语法会复杂一点,但是在未变异情况下可以工作,我去搜一下AMD的相关资料,找到一句话,不知道是不是匹配这里。
AMD同时是“匿名的”,意味着模块不需要硬编码指向其路径的引用, 模块名仅依赖其文件名和目录路径,极大的降低了重构的工作量)
2、为异步加载而设计的,主要是用于浏览器端。AMD规范则是非同步加载模块,允许指定回调函数

  • 比较:
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
2、ECMAScript 6 modules
es6 modules 的目的是创建一种能被CommonJS和AMD用户都喜欢的语法格式:
  • 与CommonJS类似,它们具有紧凑的语法、对单个导出的偏好以及对循环依赖的支持
  • 与AMD类似,它们直接支持异步加载和可配置模块加载

ES6模块超越CommonJS和AMD的地方:
  • 它们的语法比CommonJS的语法更简洁
  • 它们的结构可以进行静态分析(用于静态检查、优化等)
  • 它们对循环依赖项的支持优于CommonJS

ES6模块标准分为两个部分:
  • import 和 export 语法 (即 named exports 和 default export,named import 和 default import,详情可看此文章)
  • 2、Programmatic loader API: to configure how modules are loaded and to conditionally load modules(程序化的加载API: 配置模块的加载方式并有条件地加载模块。没懂)

3、ES6模块语法概述

有两种导出:named exports(每个模块可以有多个 named exports)和 default export (每个模块只允许至多有一个)

3.1 Named exports

模块可以通过在声明前加上关键字export来导出多个东西。这些导出以它们的名称来区分,称为命名导出


如果需要,还可以导入整个模块,并通过属性表示法引用其命名的导出


相同的实现在CommonJS里如下:


3.2 default export

只导出单个值的模块在node.js社区中会经常碰碰到。但是它们在前端开发中也很常见,在前端开发中,您经常为模型提供构造函数/类,每个模块有一个模型。ECMAScript 6模块可以选择default export,这是最重要的导出值。default export 特别容易导入。


default export 是 class 的ECMAScript 6模块如下所示


注意: default export 导出的是一个匿名表达式。它将通过模块的名称来标识,如上面两个例子中的myFunc 和MyClass。

3.3 在一个模块里可以同时存在 named exports 和 default export

下面的模式在JavaScript中非常常见的: 库是一个函数,但是通过该函数的属性提供了其他服务这是很常见的,在jQuery和Underscore.js经常会碰到这种场景。
在 CommonJS 实现如下:


上面的例子如果是用es6 modules写的话就是如下:


请注意,CommonJS 实现和ECMAScript 6 实现只是大致相似。后者具有扁平结构,而前者是嵌套的。您喜欢哪种风格是一个品味问题,但是扁平风格具有静态可分析的优点(为什么这很好,将在下面解释)。CommonJS风格的部分目的似乎是需要对象作为名称空间,这种需要通常可以通过ES6模块的 named exports 来实现。

default export 可以看作是一种特别的 named export

default export 实际上只是具有特殊名称default的 named export。也就是说,下面两个表述是等价的


而下面的这两种写法也是等价的。


4、设计目标


如果你想理解ECMAScript 6模块,它有助于理解什么目标影响了它们的设计,主要分以下几个方面:
  • 默认导出的设计
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块之间的循环依赖关系

4.1 default export

模块语法表明default export “是”模块可能看起来有点奇怪,但是如果您认为一个主要的设计目标是使默认导出尽可能方便,那么这是有意义的。

4.2 静态模块结构

在当前的JavaScript模块系统中,您必须执行代码,以查明导入和导出是什么。这就是ECMAScript 6与这些系统不同的主要原因:通过将模块系统构建到该语言中,您可以从语法上强制执行一个静态模块结构。让我们先看看这意味着什么,然后看看它带来了什么好处。

模块的结构是静态的,这意味着您可以在编译时(静态地)确定导入和导出——您只需要查看源代码,不必执行它。下面是CommonJS模块如何让这成为不可能的两个例子。在第一个示例中,您必须运行代码来查找它导入的内容:


接下来您必须运行代码来查找它导出的内容
ECMAScript 6给了你较少的灵活性,它强迫你保持静态。因此,您将获得的几个好处,下面将对此进行描述:

优点1: 可以更快的查找导入文件内容
如果用CommonJS的方式引入一个库,就会返回一个对象


因此,通过这种方式导入库, lib.someFunc 意味着你必须进行属性查找,因为它是动态的,所以会更慢。
相反,如果你用es6的方式引入一个库,您可以静态地了解其内容并优化访问。


优点2: 变量检查
使用静态模块结构,您总是静态地知道哪些变量在模块内的任何位置可见:

  • 全局变量: 唯一完全的全局变量将来自语言本身。其他一切都将来自模块(包括来自标准库和浏览器的功能)。也就是说,您静态地知道所有全局变量
  • 模块导入:您也静态地知道这些
  • 模块局部变量:可以通过静态检查模块来确定。
这有助于检查给定标识符是否正确。这种检查是JSLint和JSHint等的一个流行特性;在ECMAScript 6中,大部分可以由JavaScript引擎执行。

优点3: 为宏的支持做准备
宏仍然在JavaScript的未来路线图上。如果JavaScript引擎支持宏,可以通过库向其添加新语法,sweet.js是一个实验性的JavaScript宏系统,下面是来自The Sweet网站的一个例子: 一个类的宏。


对于宏,JavaScript引擎在编译之前执行预处理步骤:如果解析器生成的令牌流中的令牌序列与宏的模式部分匹配,则由宏体生成的令牌替换。预处理步骤只有在能够静态地找到宏定义时才有效。因此,如果您想通过模块导入宏,那么它们必须具有静态结构。

优点4: 为类型系统做准备
静态类型检查强加了类似于宏的约束:只有在静态地找到类型定义时才能执行。同样,只有具有静态结构的模块才能导入类型。
类型之所以吸引人,是因为它们支持JavaScript的静态类型快速方言,在这种方言中可以编写性能关键型代码。一种方言是 Low-Level JavaScript (LLJS) 。它目前被编译成 asm.js.。

优点5: 支持其他语言
如果您希望支持将带有宏和静态类型的语言编译成JavaScript,那么JavaScript的模块应该具有静态结构,原因见前两节。

(以上翻译的几点不是很好理解的话可以移步这里 hax.iteye.com/blog/182904…)
(同时可以参考 calculist.org/blog/2012/0…)

4.3 支持同步和异步加载

ECMAScript 6模块必须独立于引擎是同步加载模块(例如在服务器上)还是异步加载模块(例如在浏览器中)。它的语法非常适合同步加载,异步加载是由它的静态结构支持的:因为您可以静态地确定所有导入,所以您可以在评估模块体之前加载它们(让人想起AMD模块的方式)。

4.4支持模块之间的循环依赖

如果A(可能是间接/直接的)导入B和B导入A,那么两个模块A和B是循环依赖的,如果可能的话,应该避免循环依赖,它们导致A和B紧密耦合——它们只能一起使用和改变。

为什么需要支持循环依赖?
循环依赖本身并不是坏事。特别是对于对象,有时甚至需要这种依赖性。例如,在一些树(如DOM文档)中,父节点引用子节点,子节点引用父节点。在库中,通常可以通过仔细设计来避免循环依赖。但是在大型系统中,它们可能会发生,特别是在重构期间。如果模块系统支持它们,那么它将非常有用,因为在重构时系统不会崩溃。

node.js文档承认循环依赖的重要性,Rob Sayre提供了额外的证据( mail.mozilla.org/pipermail/e…)。原文列了两个人的语言,反应循环引用的必要。
让我们看看CommonJS和ECMAScript 6是如何处理循环依赖关系的。

在commonJS里的循环依赖

在CommonJS中,如果模块B引入当前正在计算其主体的模块a,它将返回A当前状态下的导出对象(下例中的第1行)。这使B能够引用其导出中该对象的属性(第2行)。属性在B的处理完成后填写,此时B的导出工作正常。


作为通用规则,请记住,对于循环依赖项,您不能访问模块主体中的导入。这是这种现象固有的,不会随着ECMAScript 6模块的改变而改变。
CommonJS方法的局限性是:
1、在node.js里,多个值的时候是不能导出一个单独的值的,只能导出对象。你可以像下面这样:
module.exports = function () { ... }

如果在模块A中这样做,就不能在模块B中直接使用导出函数,因为B的变量A仍然引用A的原始导出对象。

2、不能直接使用命名导出。也就是说,模块B不能像这样导入a.foo:
var foo = require('a').foo;
foo是没有定义的。换句话说,您别无选择,只能通过导出对象a引用foo。

CommonJS有一个独特的特性:可以在导入之前导出。保证在导入模块的主体中可以访问这些导出。也就是说,如果A这么做了,它们就可以进入B的体内。然而,导入前导出很少有用。
可以在node的官网上看到这种例子( nodejs.org/api/modules…);

在es6里面的循环引用
为了消除上述两个限制,ECMAScript 6模块导出是模块的引用,而不是值。也就是说,到模块主体中声明的变量的连接仍然是活动的。下面的代码演示了这一点。


因此,面对循环依赖关系,您是直接访问命名导出还是通过其模块访问命名导出并不重要:在这两种情况下都存在间接关系,而且它总是有效的。

5、关于导入和导出的一些知识

5.1 导入

es6 提供了以下导出方式。


5.2 导出

有两种方法可以导出当前模块中的内容。一种可以用关键字export标记声明。


默认导出的“操作数”是一个表达式(包括函数表达式和类表达式)。


另一种,您可以在模块末尾列出您想导出的所有内容(这在风格上再次与显示模块模式类似)。


你也可以导出不同的名称:
请注意,您不能使用保留字(如 default 和 new )作为变量名,但您可以使用它们作为出口的名字(您也可以使用它们作为属性名称在ECMAScript 5)。如果你想直接导入这样的命名导出,你必须为他们重命名。
5.3 重新导出
重新导出意味着将另一个模块的导出添加到当前模块的导出中。您可以添加其他模块的所有导出:


或者你可以更有选择性(可选的重命名):


6、eval() 和 modules

eval()不支持模块语法。它根据脚本语法规则解析它的参数,脚本不支持模块语法(稍后解释原因)。如果您想计算模块代码,您可以使用模块加载器API(下面将介绍)。

注: (5、6两节里关于模块导入导出和循环依赖的知识点可以看一下我写的一篇相关文章。https://juejin.im/post/5c1e58326fb9a049a570e3d5)

7、ECMAScript 6模块加载器API

除了用于处理模块的声明性语法之外,还有一个编程API。它允许你:
- 以编程方式处理模块和脚本
- 配置加载模块

加载器处理解析模块说明符(import…from末尾的字符串id)、加载模块等。它们的构造函数是Reflect.Loader。每个平台在全局变量系统(系统加载器)中保存一个定制的实例,该实例实现其特定的模块加载样式

7.1 导入模块和加载脚本


您可以通过基于 ES6 promises的API以编程方式导入模块:


System.import()使您能够:
- 在<script>元素中使用模块(不支持模块语法的地方,请参阅 Further information)。
- 按需加载加载模块
import()检索单个模块,可以使用Promise.all()导入多个模块


其他的加载方法:
- System.module(source, options?)将源中的JavaScript代码处理为模块(这是通过利用promise的异步处理)
- System.set(name, module) 用于注册一个模块(例如,您通过System.module()创建的模块)。
- System.define(name, source, options?)既计算源代码中的模块代码,也注册结果

7.2 配置加载模块

模块加载器API具有用于配置的各种钩子。这项工作仍在进行中。浏览器的第一个系统加载程序目前正在实现和测试中。其目标是找出如何最好地使模块加载可配置。
加载器API将允许对加载过程进行许多定制。例如:
1、导入的Lint模块(例如,通过JSLint或JSHint)。
2、在导入时自动转换模块(它们可以包含CoffeeScript或TypeScript代码)。
3、使用遗留模块(AMD, Node.js)。
可配置模块加载是node.js和CommonJS都有所限制的领域。

8.进一步的信息(这里是其他的一些延伸,感兴趣的可以去看看作者的其他文章)

以下内容回答了两个与ECMAScript 6模块相关的重要问题:我今天如何使用它们?如何在HTML中嵌入它们?

- “ Using ECMAScript 6 today” 这篇文章概述了ECMAScript 6,并解释了如何将它编译成ECMAScript 5。如果你对后者感兴趣可以阅读 Sect. 2。一个有趣的最小解决方案是 ES6 Module Transpiler ,它只将ES6模块语法添加到ES5中,并将其编译到AMD或CommonJS中。
- 在HTML中嵌入ES6模块:<script>元素中的代码不支持模块语法,因为元素的同步特性与模块的异步性不兼容。相反,您需要使用新的<module>元素。在 ECMAScript 6 modules in future browsers这篇博客里,作者有解释 <module> 的工作原理。它有几个显著的优势,并且可以在其替代版本<script type="module">中进行填充。
- CommonJS vs. ES6: Yehuda Katz著的 JavaScript Modules是ECMAScript 6模块的快速入门。特别有趣的是文章的 second page ,其中CommonJS模块与ECMAScript 6版本并排显示。做了一些对比。

9.ECMAScript 6模块的好处

乍一看,将模块构建到ECMAScript 6中似乎是一个无聊的特性——毕竟,我们已经有了几个很好的模块系统。但是ECMAScript 6模块有一些您无法通过库添加的特性,比如非常紧凑的语法和静态模块结构(这有助于优化、静态检查等)。他们也将——希望——结束目前占主导地位的CommonJS和AMD之间的分裂。

对模块有一个单一的本地标准意味着:
- 不再使用UMD( Universal Module Definition):UMD是一个模式名,它允许多个模块系统(例如CommonJS和AMD)使用相同的文件。一旦ES6成为唯一的模块标准,UMD就过时了。
- 新的浏览器api成为模块,而不是导航器的全局变量或属性
- 不再将对象作为名称空间:在ECMAScript 5中,Math和JSON等对象用作函数的名称空间。将来,这些功能可以通过模块提供。

出自:http://2ality.com/2014/09/es6-modules-final.html#eval-and-modules(2ality – JavaScript and more)


转载于:https://juejin.im/post/5c1e55005188254fb2766028

 类似资料: