深入ES6 模块系统

综合技术 2017-10-26

ES6 模块系统

在ES6之前,我们用自己的方式来在 JavaScript 中实现模块。很长一段时间以来,像 RequireJS、Angular 的依赖注入和 CommonJS 这样的系统,配合着一些有用的工具,比如 Browserify 和 Webpack,一直在解决我们的需求。然而,到了2015 年,一个标准的模块系统早就应该发布了。我们马上就会看到,你很快会注意到 ES6 模块受到了 CommonJS 的很大影响。我们将查看和语句,从中会看到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
    and fn.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是否和它的庞大一样方便。

您可能感兴趣的

JavaScript Error继承踩坑记 Error ES6 Class继承 在Web App中,我们通常会创建自定义错误类来区分错误类型。如果使用ES6的Class语法,那么应该有类似如下写法: class MyError extends Error { constructor (msg) { supe...
如何在ES5与ES6环境下处理函数默认参数... 函数默认值是一个很提高鲁棒性的东西(就是让程序更健壮) MDN关于函数默认参数的描述:函数默认参数允许在没有值或 undefined 被传入时使用默认形参。 ES5 使用逻辑或 || 来实现众所周知,在ES5版本中,并没有提...
深入理解ES6之——JS类的相关知识 基本的类声明 类声明以class关键字开始,其后是类的名称;剩余部分的语法看起来像对象字面量中的方法简写,并且在方法之间不需要使用逗号。 class Person { //等价于prototype的构造器 constructor(name) { this.na...
ES6数组常用方法 经历了几天并不开心的日子,应试教育的无奈又一次摆在我眼前。哦,对了,我还是个非985&211大学的学生。 废话不应该那么多? 还是说一说ES6的数组吧... 无奈抑郁系列配图 Array.from()方法 用于将类数组和可遍历的对象转换为真...
ECMAScript Promise对象 Promise的含义Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。 Promise 对象...