ECMAScript 6 Modules(模块)系统及语法详解
在2014年7月底,TC39 [1]又召开了一次会议,在此期间ECMAScript 6(ES6)模块语法的最后细节被最终确定。这篇博客文章概述了完整的 ES6 模块系统。
当前的 JavaScript 模块系统
愚人码头注:原文日期为 2014-09-07 ,请用当时的环境浏览本段内容
JavaScript 目前没有内置支持模块化,但社区创造了令人印象深刻的解决方案。两个最重要的(不幸的是互不兼容)标准是:
CommonJS 模块:这个标准的主要实现在Node.js中(Node.js模块有一些超越 CommonJS 的功能)。特点:
语法简洁
为同步加载设计
主要用途:服务器端
异步模块定义(AMD):这个标准最流行的实现是 RequireJS。 特点:
稍微复杂的语法,使AMD能够在没有 eval()(或编译步骤)的情况下工作。
专为异步加载而设计
主要用途:浏览器
以上只是对当前状况的大致的解释一下。如果你想要更深入的资料,可以看看Addy Osmani的文章 使用AMD编写模块化JavaScript,CommonJS & ES Harmony。
ECMAScript 6 模块
ECMAScript 6模块的目标是创建一个格式,使 CommonJS 和 AMD 的用户都满意:
与 CommonJS 类似,简洁的语法,倾向于单一的接口并且支持循环依赖。
与AMD类似,直接支持异步加载和可配置的模块加载。
内置语言允许ES6模块超越 CommonJS 和 AMD(细节将在后面解释):
他们的语法比 CommonJS 更简洁。
他们的结构可以静态分析(用于静态检查,优化等)。
他们支持的循环依赖性优于 CommonJS。
ES6模块标准有两个部分:
声明语法(用于导入和导出)
编程式加载器(loader)API:配置如何加载模块以及有条件地加载模块
ES6模块语法概述
有两种导出方式:命名的导出(每个模块可以导出多个)和 默认的导出(每个模块仅导出一个)。
命名的导出(每个模块多个)
模块可以通过使用前缀关键词 export
声明来导出多个东西。这些导出由其名称进行区分,并称为命名的导出。
还有其他方法来指定命名的导出(稍后解释),但我觉得这个方式很方便:如果没有外层环境,你可以只关注你的代码,然后以你想要的关键词来标识所有的东西。
如果你需要,您还可以 导入(import) 整个模块,并通过属性符号引用其命名的导出(export) :
在 CommonJS 语法中的相同代码:有段时间我在 Node.js 下尝试了几种不错的策略,以减少我的模块导出的冗余代码。现在我喜欢以下简单但略微冗余的风格,让人联想到暴露式模块模式:
默认的导出(每个模块一个)
在 Node.js 社区中,只导出单个值的模块非常受欢迎。但是它们在前端开发中也很常见,你经常用 构造函数 / 类 来创建模型,每个模块有一个模型。ECMAScript 6模块可以选择默认的导出方式,导出最主要的值。默认的导出方式特别容易导入。
以下 ECMAScript 6模块 是 单个函数:
默认导出一个类的 ECMAScript 6 模块如下所示:
注意:定义式导出声明的运算对象是一个表达式,往往不需要名字。相反,它将通过其模块的名称来标识。
在一个模块中同时具有命名的导出和默认的导出
以下模式在 JavaScript 中非常常见:一个库是单个函数,但是通过该函数的属性提供其他功能。例如 jQuery 和 Underscore.js 。 以下是 Underscore 作为 CommonJS 模块的大致写法:
使用 ES6 眼光去看,函数_
是默认的导出,而each
和forEach
均为命名的导出。事实证明,您实际上可以同时具有命名的导出和默认的导出。举个例子,例如之前的 CommonJS 模块,以 ES6 来重写模块,看起来像这样:
注意 CommonJS 版本和 ECMAScript 6 版本只是大致相似。后者具有扁平式结构,而前者是嵌套式结构。喜欢哪一种风格有自己决定,但是扁平式结构具有静态可分析的优点(为什么这样好下面会解释)。CommonJS风格看起来为了满足对象的需要部分被作为命名空间,这种需求通常可以通过 ES6 模块和命名导出来实现。
需要注意的是 CommonJS 版本和 ECMAScript 6 版本仅仅是大致相同。后者为扁平式结构,而前者为嵌套式结构。喜欢哪一种风格有自己决定,不过扁平式结构具有做静态分析的优势(下面会提到优点)。CommonJS 风格看起来为了满足对象的需要部分被作为命名空间,可以通过 ES6 模块来实现这种需求并且导出实现。
默认导出只是另一个命名导出
默认导出实际上只是一个具有特殊名称default
的命名导出。也就是说,以下两个语句是是等价的:
类似地,以下两个模块也是等价的默认导出:
为什么我们需要命名导出?
你可能想知道 - 如果我们可以简单地默认导出对象(如CommonJS),那为什么我们需要命名导出?答案是,你不能通过对象强制实施一个静态结构,并且会失去所有相关的优点(在下一节中描述)。
设计目标
如果你想了解 ECMAScript 6 模块设计理念,那么首先需要了解什么目标影响了他的设计。主要是以下几点:
默认导出是有利的
静态模块结构
同时支持同步和异步加载
支持模块之间的循环依赖性
以下小节解释这些目标。
默认导出是有利的
模块语法显示,默认导出可能导致模块看起来有点奇怪,但它是有道理的,你考虑一个主要的设计目标是使默认导出尽可能方便。引用David Herman的话说:
ECMAScript 6 支持单个/默认导出风格,并提供默认导入语法糖。导入命名的导出显得不太简洁。
静态模块结构
愚人码头注:关于 静态模块结构 可以看看这篇文章 webpack 2中的Tree Shaking,有助于更好的理解。
在当前的JavaScript模块系统中,你必须执行代码,来找出什么是 导入 和 什么是 导出。这是 ECMAScript 6 与这些模块系统(愚人码头注:指 CMD,AMD)决裂的主要原因: 通过将模块系统构建到JavaScript语言中,您可以在语法上强制执行静态模块结构。让我们先来看看这意味着什么,带来什么好处。
模块的静态结构,意味着您可以在编译时确定导入和导出(静态) - 你只需要看看源代码,你不必执行它。下面是两个 CommonJS 模块的例子,告诉你为什么 CommonJS 模块在编译时确定导入和导出是不可能的。在第一示例中,你必须运行代码才可以找出它导入的是什么:
在第二个示例中,您必须运行代码才可以找出它导出的内容:
ECMAScript 6 模块的灵活性不如 CommonJS 模块,强迫使用静态结构。但却使你得到几个好处(参考引用 David Herman 的“Static module resolution”),下面描述。
好处1:更快的查找
如果你在 CommonJS 中 require
一个库,你会得到一个对象:
因此,通过lib.someFunc
访问命名导出意味着您必须进行属性查找,这是很慢,因为它是动态的。
相反,如果您在 ES6 中导入一个库,您可以静态地了解其内容并可以优化访问:
好处2:变量检查
利用静态模块结构,你总是静态地知道哪些变量在模块内的任何位置是可见的:
全局变量:越来越多,唯一完全的全局变量将将来自适当的语言。一切都将来自模块(包括来自标准库和浏览器的功能)。也就是说,你静态地知道所有的全局变量。
模块导入:你也能静态地知道。
模块局部变量:可以通过静态检查模块来确定。
这有助于检查给定的标识符是否拼写正确。这种检查是程序检测器中一个受欢迎的特性,如JSLint和JSHint; 而在 ECMAScript 6 中,大多数可以由 JavaScript 引擎执行。
此外,还可以静态检查命名导入(例如lib.foo
)的任何访问。
好处3:为宏命令做准备
宏命令仍然是JavaScript未来的未来。如果JavaScript引擎支持宏命令,你可以通过一个库添加新的语法。Sweet.js是JavaScript一个实验性的宏系统。下面是Sweet.js网站的一个例子:一个类的宏。
对于宏来说,JavaScript引擎在编译之前执行预处理步骤:如果由解析器产生的token流中的token序列与宏的模式部分匹配,它被由宏的主体生成的token替换。只有当您能够静态地找到宏定义时,预处理步骤才有效。 因此,如果你想通过模块导入宏,那么它们必须有一个静态结构。
好处4:为类型做准备
静态类型检查强加类似于宏的约束:它只能在可以静态找到类型定义时才能完成。同样,只有当模块具有静态结构时,才能从模块导入类型。
类型是吸引人的,因为它们支持静态类型的JavaScript的快速dialect,其中可以编写性能关键代码。一种这样的dialect是低级JavaScript(LLJS)。它目前编译为asm.js.
好处5:支持其他语言
如果你想支持编译语言的宏和静态类型的JavaScript,JavaScript的模块应该有一个静态结构,因为前两节提到的原因。
同时支持同步和异步加载
ECMAScript 6 模块必须能独立于引擎加载模块,不论是否同步地(例如在服务器上)或异步地(例如在浏览器中)。它的语法非常适合于同步加载,通过其静态结构启用异步加载:因为你可以静态确定所有导入,您可以在评估模块的主体之前加载它们(这种让人联想到AMD模块的方式)。
支持模块之间的循环依赖性
如果两个模块 A 和 B ,A(可能间接)导入 B,并且 B 导入 A ,那么模块 A 和 B 相互依赖。如果可能,应避免循环依赖,因为这样会导致 A 和 B 紧密耦合 - 它们只能一起使用和改进。
为什么支持循环依赖?
循环依赖不是天生就是邪恶的。特别是对于对象来说,你有时甚至想要这种依赖。例如,在一些树(例如DOM文档)中,父元素引用子元素,并且子元素引用回父元素。在库中,通常可以通过仔细设计避免循环依赖。但在大型系统中,它们可能发生,特别是在重构过程中。然后,如果模块系统支持它们是非常有用的,因为当你重构时,系统不会中断。
Node.js文档承认循环依赖的重要性(查看 Node.js API 文档中的“Modules: Cycles”),并且Rob Sayre提供了额外的证据:
数据点:我曾经为Firefox实现了一个类似[ECMAScript 6 modules]的系统。我被要求循环依赖支持3周后发布。
Alex Fritze 发明的系统,我工作起来不完美,并且语法不是很漂亮。但它仍然被使用7年后,所以它必须得到解决。
让我们看看 CommonJS 和 ECMAScript 6 如何处理循环依赖。
CommonJS中的循环依赖
在CommonJS中,如果模块 B require
主体当前正在被评估的模块 A,它会回到其当前状态下模块 A的出口对象(以下示例中的行#1)。这使得 B 能够引用该对象内部导出(行#2)的属性。在 B 的评估完成后填充属性,此时 B 的导出能工作正常。
作为基本规则,请记住,使用循环依赖关系,您无法访问模块主体中的导入。这是现象固有的,并且不随 ECMAScript 6 模块而改变。
CommonJS方法的局限性是:
Node.js风格的单值导出不能工作。 在Node.js中,您可以导出单个值而不是对象,如下所示:
module.exports = function(){...}
如果你在模块A中这么做了,你将无法使用模块B中的导出函数,因为B中的变量a
仍然引用A的原始导出对象。您不能直接使用命名导出。 也就是说,模块B不能像这样导入
a.foo
:var foo = require('a').foo;
foo
将简单地未定义。 换句话说,你别无选择,只能通过导出对象a
引用foo
。
CommonJS有一个独特的功能:您可以在导入之前导出。这样的导出保证可以在导入模块的主体中访问。也就是说,如果A这样做,他们可以在B的主体中访问。但是,在导入之前导出很少有用。
ECMAScript 6中的循环依赖
为了消除上述两个限制,ECMAScript 6 模块导出绑定,而不是值。也就是说,在模块体内声明的变量是保持活动的。这可以通过以下代码演示。
因此,面对循环依赖,无论是直接访问命名导出还是通过其模块访问命名导出:在这2种情况下,只要有一个间接引用,它总是能正常工作。
更多关于导入和导出
导入
ECMAScript 6 提供了以下的导入方式(参见 ECMAScript 6 规范中的 Imports):
导出
有两种方法可以导出当前模块中的内容(参见 ECMAScript 6 规范中的 Exports)。 第一种是,您可以使用关键字 export
来声明。
默认导出(愚人码头注:通过关键字default
声明)的运算对象是一个表达式(包括函数表达式和类表达式)。 例如:
第二种是,您可以在模块的末尾列出要导出的所有内容(风格上与模块模式类似)。
您也可以使用不同的名称导出:
请注意,您不能使用保留字(如default
和new
)作为变量名称,但您可以将其用作导出的名称(在 ECMAScript 5 中,您也可以将它们用作属性名称)。如果要直接导入此类命名的导出,那么你必须将它们重命名为正确的变量名称。
重新导出
重新导出意味着将另一个模块的导出添加到当前模块的导出。 你可以添加所有其他模块的导出:
或者你可以有更多选择性(随意地重命名):
eval() 和 模块
eval()
不支持模块语法。它根据脚本语法规则解析其参数,而脚本不支持模块语法(原因稍后解释)。如果要评估模块代码, 您可以使用模块加载器API(如下所述)。
ECMAScript 6 模块加载器 API
除了使用模块的声明性的语法外,还有一个编程式的API。 它允许您:
以编程方式使用模块和脚本
配置模块加载
加载器处理解析 模块说明符(在 import...from
后面的字符串 ID)加载模块,等。他们的构造函数是Reflect.Loader
。每个平台在全局变量 System
中保留自定义实例(系统加载器),实现其平台特定的模块加载方式。
导入模块并加载脚本
您可以通过基于 ES6 promises的 API 以编程方式导入模块:
System.import()
使你可以:
在
<script>
元素中使用模块(不支持模块语法,有关详细信息,请参阅“更多信息”部分)。有条件地加载模块。
System.import()
检索单个模块,您可以使用Promise.all()
来导入多个模块:
更多加载器方法:
System.module(source, options?) 计算
source
中的JavaScript代码到模块(通过promise异步传递)。System.set(name, module) 用于注册一个模块(例如,您通过
System.module()
创建的一个模块)。System.define(name, source, options?) 都会评估
source
中的模块代码并注册结果。
配置模块加载
模块加载器 API 具有用于配置的各种 hook(钩子) 。目前它仍在发展中。用于浏览器的第一系统加载器正在实施和测试。目标是找出如何最好地配置模块加载。
加载器API将允许在很大程度上定制加载过程。例如:
Lint模块导入(例如通过JSLint或JSHint)。
在导入时自动转译模块(它们可能包含 CoffeeScript 或 TypeScript 代码)。
使用旧版模块( AMD,Node.js )。
可配置模块加载是 Node.js 和 CommonJS 受限的一个领域。
更多信息
以下内容回答与 ECMAScript 6 模块相关的两个重要问题:现在我如何使用他们?如何将它们嵌入到HTML中?
愚人码头注:
由于作者写这篇文章的时间较早,有些观点和资源现在已经不适用了。
比如,现在流行的 ES6 转换器有 Babel ,Buble , Rollup.js 等等。
现在很多框架已经支持 ES6 语法,项目一般都通过打包工具(例如:webpack )打包发布。
所以现在使用 ES6 已经非常普遍简单了,不需要有太多顾虑。
“今天开始使用 ECMAScript 6” 概述了 ECMAScript 6 的使用,并解释如何将其编译为 ECMAScript 5 。如果你对后者感兴趣,请先阅读Sect.2。 一个简便使用的解决方案是使用ES6模块转换器(愚人码头注:该项目已经废弃)将 ES6 模块语法添加到 ES5 并将其编译为 AMD 或 CommonJS 。
将ES6模块嵌入HTML中:
<script>
元素中的代码不支持模块语法,因为元素的同步性质与模块的异步性不兼容。相反,您需要使用新的<module>
元素。博客文章“ECMAScript 6 模块在未来的浏览器”解释了<module>
是如何工作的。它与比<script>
相比有几个显着的优点,可以在其替代版本<script type="module">
中进行 polyfill。CommonJS vs. ES6:“JavaScript模块”(作者Yehuda Katz)是 ECMAScript 6 模块的快速介绍。特别有趣的是第二页,CommonJ S模块与 ECMAScript 6 版本并排显示。
ECMAScript 6模块的优点
乍一看,ECMAScript 6 中内置的模块看起来可能是一个无聊的功能 - 毕竟我们已经有了几个好的模块系统。但 ECMAScript 6 模块具有无法通过库添加的功能,例如非常紧凑的语法和静态模块结构(这有助于优化,静态检查等)。他们还有望结束当前主流标准 CommonJS 和 AMD 之间的分裂。
具有独立的原生标准对于模块来说意味着:
无需更多UMD(通用模块定义):UMD是模式的名称,即使得相同的文件能够被多个模块系统(例如 CommonJS 和 AMD )使用。一旦ES6是唯一模块成为标准,UMD已过时。
新的浏览器API成为模块,代替全局变量或引导属性。
没有更多的对象命名空间:对象如Math和JSON作为ECMAScript 5中函数的命名空间。在将来,这样的功能可以由模块提供。
英文原文:《ECMAScript 6 模块:最终语法》
建议继续学习:
- 在Apache2.2.XX下安装Mod-myvhost模块 (阅读:11815)
- Nginx模块开发入门 (阅读:9996)
- nginx模块开发 (阅读:4705)
- Hive源码解析-之-语法解析器 (阅读:4343)
- CommonJS 的模块系统,AMD 和 Wrappings, 以及 RequireJS (阅读:4234)
- 搭好了apache模块的开发环境 (阅读:3549)
- PHP 模块编写需要注意的一个问题---- php模块及函数名都定义成小写吧 (阅读:3419)
- 使用 Perl 来开发 Nginx 的模块 (阅读:3351)
- FarmVille(美版开心农场)谈架构:所有模块都是一个可降级的服务 (阅读:3334)
- php无法加载pcre.so的解决办法 (阅读:3068)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:愚人码头 来源: WEB前端开发
- 标签: ecmascript modules 模块 语法
- 发布时间:2017-02-19 23:52:34
- [68] IOS安全–浅谈关于IOS加固的几种方法
- [67] Twitter/微博客的学习摘要
- [63] 如何拿下简短的域名
- [63] android 开发入门
- [63] Go Reflect 性能
- [61] find命令的一点注意事项
- [60] Oracle MTS模式下 进程地址与会话信
- [59] 流程管理与用户研究
- [58] 【社会化设计】自我(self)部分――欢迎区
- [56] 图书馆的世界纪录