在ES6之前,我们用自己的方式来在 JavaScript 中实现模块。很长一段时间以来,像 RequireJS、Angular 的依赖注入和 CommonJS 这样的系统,配合着一些有用的工具,比如 Browserify 和 Webpack,一直在解决我们的需求。然而,到了2015 年,一个标准的模块系统早就应该发布了。我们马上就会看到,你很快会注意到 ES6 模块受到了 CommonJS 的很大影响。我们将查看export
和 import
语句,从中会看到ES6模块和CommonJS有多一致,同时,我们将会在这篇文章中讨论它们。
今天我们将介绍 ES6 模块系统的几个方面。
严格模式
在 ES6 模块系统中, 严格模式默认被开启。如果你不知道严格模式是什么, 它只是语言的一个更严格的版本它让语言的很多不好的部分都消失了。它使编译器可以通过在用户代码中禁止使用一些不可靠的语法来表现得更好。下面是对 MDN 上的严格模式文章中所记录的更改的总结。
-
变量不能未声明就使用
-
函数参数必须有唯一的名称 (否则会被认为是语法错误)
-
with
语句被禁止使用 -
赋值给只读属性会抛出一个错误
-
像
00840
这样的八进制数是语法错误 -
尝试
delete
不可删除的数据会抛出一个错误 -
delete prop
被认为是语法错误, 只能删除属性delete global[prop]
-
eval
不会引入新的变量到它的作用域 -
eval
和arguments
的绑定不会被改变 -
arguments
不会神奇地跟踪方法参数的变化 -
不再支持
arguments.callee
,使用它会抛出TypeError
-
不再支持
arguments.caller
,使用它会抛出TypeError
-
上下文作为
this
在方法调用时不会被强制包装成一个Object
(译者注:即this不会指向全局对象) -
不再能够使用
fn.caller
andfn.arguments
访问 JavaScript 的堆栈 -
保留字(例如
protected
,static
,interface
等等)不能被作为新变量声明
如果这些规则对你来说不是显而易见的,你应该使用 'use strict'
在每一个地方。尽管在 ES6 中已经成为事实,但在 ES6 中使用 'use strict'
仍然是一种很好的做法。我已经使用严格模式很长时间了,并且绝不会用回原来的模式!
现在让我们了解export
,我们的第一个 ES6 模块关键字!
export
在 CommonJS 中,你将值暴露在module.exports
上来导出它们。正如下面的代码片段所示,您可以导出任何内容像是基本类型、对象、数组或函数。
module.exports = 1
module.exports = NaN
module.exports = 'foo'
module.exports = { foo: 'bar' }
module.exports = ['foo', 'bar']
module.exports = function foo () {}
ES6模块系统将 export
封装成API,类似于 CommonJS的modules
。ES6 模块中的声明只作用于该模块,和使用 CommonJS 一样。这意味着,在模块中声明的任何变量都不能用于其他模块,除非它们明确地导出为模块 API 的一部分(然后导入到希望访问它们的模块中)。
导出默认的绑定
你可以通过把 module.exports =
变成 export default
来模拟我们刚刚看到的CommonJS代码。
export default 1
export default NaN
export default 'foo'
export default { foo: 'bar' }
export default ['foo', 'bar']
export default function foo () {}
与 CommonJS 不同,导出语句只能放在 ES6 模块的最外层,而不能放在方法中,即使在加载模块时它们所在的方法会立即被调用。据推测,这种限制是为了让编译器更容易地解释 ES6 模块,但是这也是一个很好的限制,因为有很多很好的理由去以动态地定义和暴露 API的方式来调用方法。
function foo () {
export default 'bar' // SyntaxError
}
foo()
你不只可以使用默认的Export,你还可以使用具名的Exports。
具名的Exports
在 CommonJS 中,你甚至不需要事先分配一个对象给 module.exports
。你可以把属性添加到它上面。不管 module.exports
最终的属性包含什么,它仍然是一个单独的绑定。
module.exports.foo = 'bar'
module.exports.baz = 'ponyfoo'
我们可以通过使用具名导出语法在 ES6 模块中复制上述内容,而不是像CommonJS一样将它分配给module.exports
。在ES6中,你可以声明要export
的绑定。注意,下面的代码不能重构为先声明变量再执行 export foo
,那将会导致一个语法错误。在这里,我们看到了ES6模块如何通过声明式模块系统API的工作方式来支持静态分析。
export var foo = 'bar'
export var baz = 'ponyfoo'
还有一个重要的点,是要记住我们正在导出的是绑定。
是绑定,而不是值
重要的一点是,ES6 模块导出的是绑定,而不是值或引用。这意味着您导出的foo
变量将被绑定到模块上的foo
变量中,它的值将取决于对foo
的修改。 不过,我建议在最初加载模块之后,不要更改模块的公共接口。
如果你有一个./a
模块像下面这样,这导出的foo
将被绑定为'bar'
,持续500ms之后,foo
将绑定为 'baz'
export var foo = 'bar'
setTimeout(() => foo = 'baz', 500)
除了默认绑定和单独绑定之外,你还可以导出一个绑定列表。
绑定列表
正如下面的代码片段所示,ES6 模块允许你导出已命名的位于顶级作用域的成员列表。
var foo = 'ponyfoo'
var bar = 'baz'
export { foo, bar }
如果你想要用其他名字来导出一个绑定,你可以使用export { foo as bar }
语句,就像下面展示的这样。
`export { foo as ponyfoo }`
在使用export
的命名成员列表声明风格时,还可以使用as default
。下面代码的作用和执行export default foo
和export bar
一样,只不过在一行语句而已。
`export { foo as default, bar }`
只在模块文件的底部使用export default
有很多好处。
export
最佳实践
可以定义具名的Exports,可以导出一个具有别名的列表,还可以暴露一个默认的export
,这会导致一些混乱。在很大程度上,我鼓励你们使用export default
并且最好在模块文件的末尾使用。如下代码所示,你可以调用你的API 对象 api
或者将它命名为模块本身。
var api = {
foo: 'bar',
baz: 'ponyfoo'
}
export default api
第一,模块的导出接口立即变得明显。无需在模块中翻查并将各个部分组合在一起来计算 API,您只需滚动到最后。有一个清晰定义的 API 导出的地方,也可以更容易地解释模块导出的方法和属性。 第二,是应该使用 export default
还是具名的导出又或者是列表的导出甚至是带有别名的导出,你不应该纠结这个。现在有一个指导方针,就是在任何地方都使用 export default
。 第三,一致性。 在CommonJS世界中,我们通常从模块中导出一个方法,然后就可以了。而使用具名导出进行这样的操作是不可能的,因为你暴露了一个对象来表示该方法,除非你在导出列表中使用as default
。 第四,这实际上是之前所提到的点的总结。export default
语句放在模块的底部,我们立即可以很清晰的看出这个模块的API是什么、有哪些方法,可以让模块的使用者可以很轻松的调用它的 API。当习惯于使用export default
并总是在模块的最后使用它,你会感到使用ES6的模块系统是无痛的。 现在我们已经讨论了export
API 及其注意事项,让我们开始讨论 import
语句。
import
这个语句是和export
相对的语句。首先,它们可以被用来从另一个模块加载一个模块,这种加载模块的方式是特别实现的,目前还没有浏览器实现模块加载。聪明的人会在浏览器中解决模块加载问题,这样,你就可以立即编写符合标准的 ES6 代码。像 Babel 这样的转换工具可以在模块系统的帮助下像CommonJS一样连接模块。意味着在babel中,import
语句和CommonJS中的require
语句遵循一样的语义。
让我们以 lodash
为例。下面的语句简单地从模块中加载 Lodash 模块。它并没有创建任何变量,但它将可以使用lodash
模块。
`import 'lodash'`
在导入绑定之前,让我们来关注一下import
语句的实际情况。和export
很像,它只能定义在模块的顶级作用域。这可以帮助转换工具实现它们的模块加载功能,并帮助其它静态分析工具解析你的代码库。
Importing Default Exports
在CommonJS中,你可以通过 require
语句import
一些代码,就像这样:
`var _ = require('lodash')`
要从ES6模块导入默认的导出绑定,你只需要为它指定一个名字。与声明一个变量相比,语法有点不同,因为你正在导入一个绑定,而且可以让它更利于静态分析工具的分析。
`import _ from 'lodash'`
你也可以导入具名的导出并且可以使用别名。
导入具名的导出
这里的语法和我们刚才使用的默认导出非常相似,只需添加一些大括号,然后选择任意指定的导出. 注意,这个语法类似于解构赋值语法,但也有一些不同。
`import {map, reduce} from 'lodash'`
不同于解构赋值的是,你可以使用别名来重命名导入的绑定。你可以在你认为合适的情况下混合使用别名和非别名的导出。
`import {cloneDeep as clone, map} from 'lodash'`
你还可以混合和匹配指定的导出和默认导出。如果你想要它在括号里,你必须使用default
的名称,你可以为default
指定别名;或者你也可以将默认的导入与指定的导入列表混合在一起。
import {default, map} from 'lodash'
import {default as _, map} from 'lodash'
import _, {map} from 'lodash'
最后,还有import *
的语句
import
所有内容
你还可以将一个模块导入为命名空间对象。它不导入指定的导出或默认值,而是导入所有的东西。注意,导入语法必须使用别名,其中所有绑定都将被替换到别名上。如果有一个默认的导出,将会被替换为alias.default
。
`import * as _ from 'lodash'`
上面的代码展示了这个语法。
结论
注意,你可以在利用CommonJS模块的同时,通过babel编译器来使用ES6模块。最重要的是,你可以在CommonJS和ES6模块之间进行互操作。这意味着即使你导入了一个用CommonJs编写的模块,它也会起作用。 ES6模块系统看起来很棒,它是JavaScript中缺少的最重要的东西之一。我希望他们能很快找到一个最终完成的模块加载API和浏览器实现。你可以从一个模块中export
或import
绑定的多种方法,但这并不多,因为它们增加了复杂性,但是时间将会告诉你,所有额外的API是否和它的庞大一样方便。