ES6中的 Module
为了方便管理和维护一些复杂的项目,开发者往往会将整个大型项目细分为很多不同的功能模块。这样,模块之间便没有那么强的耦合度,如果某个模块出错,也能快速定位和单元测试。并且,正是由于模块之间的弱关联性,团队内便可以多人同时开发一个项目,而且我们可以把单个模块抽离出来,应用到其他项目中。
很多开发语言都有 模块 或者类似模块的概念,比如说,C语言中的模块是一个个小小的独立文件,应用时,只需要在主程序中 include
进来。又比如说 node.js 中模块叫做 包(package),要使用一些常见的功能,只需要在 npm官网 上下载对应的依赖包,然后再 require
到项目即可。
但是,并不是所有的语言都具有 模块 的概念。至少对于 ES6 之前的 JavaScript 来说,它是没有的。因此,如果我们要在程序中公用一些方法或者代码,要么,就把代码写在一个作用域内,要么,就把一些方法挂载在 window 全局对象上。比如,最常见的插件代码中,总会有这么一句:
window.plugInName = plugInName; |
但是,我们都知道,全局变量会导致代码冲突或者安全问题。
于是,ES6 就出现了 模块(Module)。
简单来说,模块 有点类似 script
标签的使用,模块这个文件也就是 script
中 src
属性所引用的文件。但它们两者还有一些区别:
- 模块的下载和执行顺序取决于它们引用的先后位置
- 模块中的代码,默认是处于严格模式,你无法手动退出这个模式
- 模块的顶层对象(this)不是
window
,而是undefined
- 模块可以只导入需要的部分,而不用加载整个模块
在了解模块的相关内容之前,需要注意的是,目前主流浏览器还没有支持 module 的,但你可以通过相关编译工具将它转换成 ES5 代码。
一、export(导出)的使用
为了让当前模块所定义的内容可以在其他模块中使用,我们必须使用 export
命令来导出它们。比如,导出一个变量:
export var name = 'yix'; |
在上面代码中,我们只是在变量声明的前面加上关键词 export
,便完成了导出。你也可以像下面这样导出多个变量:
export var name = 'yix'; |
或者,采用先定义,再一次性导出的形式:
var name = 'yix'; |
要注意的是,这里用 {}
大括号将导出的内容包含起来。
除了变量,我们还可以导出函数、对象、类等,如:
// 导出一个变量 |
假设上面模块名为 test.js
,后文中提到的 test.js
都是此处所定义的。
无论是导出单个内容、还是导出多个内容,它们都需要指定标识符(变量名、函数名等)。但有的时候,我们可能不希望逐个指定标识符导出,我们需要导出整个模块。比如在 main.js
里导出整个 test.js
模块,则可以采用如下写法:
export * from './test.js'; |
二、import(导入)的使用
为了能够在当前模块可以使用其他模块所定义的内容,我们必须首先使用 import
命令来导入它们。比如,要导入 test.js
中的变量 name
,你可以这样:
import {name} from './test.js'; |
通过上面代码,我们可以看到,import
后面跟着的是 {}
大括号,然后大括号中包含了我们需要导入的变量,它实际上是模块中的一个引用。它的名称和在 test.js
里定义的是一样的,于是,我们便可以在当前模块使用这个变量了。
而大括号后面是跟着关键词 from
,它后面是导入模块的路径,表示该变量是从某某路径的模块所导入而来的。
说到导入模块的路径,它可以使用绝对路径,也可以使用绝对路径。假设当前模块是 main.js
,需要导入的模块名为 test.js
。那么可以总结为以下四种情况:
- url 格式,比如
https://www.xx.com/test.js
- 以
/
开头的,比如/test.js
,表示从main.js
所在项目根目录查找test.js
- 以
./
开头的,比如./test.js
,表示从main.js
所在目录查找test.js
- 以
../
开头的,比如../test.js
,表示从main.js
父级目录开始查找test.js
和 导出 一样,我们也能同时导入多个引用,如:
import {name, add, person} from './test.js'; |
在这里,我们导入了三个引用,分别为:变量 name
、函数 add
、对象 person
,它们之间用逗号隔开。
在某些特殊情况下,可能需要导入整个模块。我们不会在 {}
大括号中逐个列出 test.js
定义的变量、方法或者类,因为我们有更便捷的方式,即:
import * as xx from './test.js'; |
上面的写法,可能与我们之前遇到的都不大相同。这里首先用到了 *
符号,表示全部的意思,然后接着是一个关键词 as
,之后是 xx
(这个 xx
是由你自定义名称),而后面的 from
和前面是一样的。
以上格式为固定写法,值得一说就是这个我们自定义的 xx
。我们并没有在 test.js
定义 xx
,但是它却以对象的形式存在当前模块中,而 test.js
模块中导入的内容都以属性的形式挂载到了这个 xx
对象上,这样,我们便可以在当前模块使用 test.js
所导出的内容。总之,你可以简单把 xx
理解为访问 test.js
整个模块的钩子。
另外,主要注意的是,如果多次使用 import
从同一个模块中导入多个内容,这个模块只会加载并执行一次,所以以下两种写法是等价的:
import {name} from './test.js'; |
三、as 的使用
其实在多人开发中,很多模块可能不是我们自己写的,我们只需要将一些需要模块导入进来即可。这些模块中定义的内容在命名上可能不太符合语义,那么,我们会觉得如果在导入模块时,能改变原有的名称(比如变量,函数名等。内部的功能实现不变)就好了。
有没有办法实现呢?答案是肯定的。
ES6 模块中提供了关键词 as
,它的功能主要是在模块外部重新指定引用名称。
比如,前面前文中提到的 test.js
模块,这里面有这么一个函数:
// 导出一个函数 |
在使用这个函数时,我觉得它的函数名为 sum 更为合适。于是,我们要把 add
更改为 sum
,这里有两种方法。
第一种是在导出模块 test.js
中处理,即先定义,再命名导出:
function add(a, b) { |
然后使用时,直接使用该函数名:
import {sum} from "./test.js"; |
这种做法可能会被人骂。原因有两个,其一,这些模块通常都是别人编写的,擅自改写别人代码,或许会引发他们的不满(命名这种东西,没有对错)。其二,这个模块可能是公用的,别人的项目代码已经导入并使用其中的功能了,如果你导出时将 add
修改为了 sum
,那使用导入使用时,就必须用 sum
函数,但是,之前的项目代码还是用的 add
,这必然会引发错误。
而另外一种方法则相对好多了。因为它无需修改原有的模块代码,只是在使用导入模块时,重命名引用即可,如:
import {add as sum} from './test.js'; |
四、默认值
通过前面的内容,我们知道,无论是 export(导出)还是 import(导入),都必须指定对应的标识符,即引用的名称。
我们可以不指定标识符,只要应用模块的默认值即可。比如导出一个匿名函数:
export default function(a, b) { |
由上面的代码可以知道,采用默认值导出的方式,需要在 export
后面加上关键词 default
。此时,整个模块对外的接口,便是这个求和函数。
当然,我们也可以先声明函数,再进行默认输出,如:
function add(a, b) { |
此时,需要注意的是,add
无需用 {}
大括号包括起来。结合前面说到的 as
重命名,上面代码块的最后一行也可以写成这样:
export {add as default}; |
类似的,我们也可以在导入时应用默认值。比如,导入默认值为某个函数:
import add from './test.js'; |
可以看到,默认值导入的写法与基本导入有点类似,但这里的 add
没有被 {}
包括起来。
另外,在同一 import
语句,你不仅能导入默认值,还能导入普通引用。只需通过如下写法:
import add, {name} from './test.js'; |
上面的代码中,既导入了默认值,也导入了普通引用。
要注意的是,默认值必须写在普通引用前,它们之间用 ,
分割开来。默认值外面没有 {}
大括号,而普通引用仍然还是被 {}
大括号所括起来。如果你想要保留默认值,并且把它们都放入 {}
大括号中,你可以使用 as
重名,像这样:
// 等价于上面代码的第一行 |
在某些情况下,我们会在模块中导出之前导入的模块,比如:
import {add, name} from './test.js'; |
上面的代码,从 test.js
模块中导入了 add
函数和 name
变量,接着又把这两个引用导出。根据模块的特性,我们也可以是用一行 export
语句来实现它:
// 等价于上面两行代码 |
五、加载模块
默认情况下,<script>
元素用于加载脚本,当没有指定 type
属性时,该元素主要用于存放或者加载 Javascipt 文件。我们可以将脚本代码内嵌在其中,也可以结合 src
属性来外链 JavaScript 文件。
5.1 网页中使用模块
ES6 也是用 <script>
标签在网页中加载模块,它也有 defer
(延迟加载)和 async
(异步加载)这样的属性。不同的是,你必须设置 <script>
元素的 type
属性值为 module
,这样浏览器通过这个属性值才能分辨出它是一个模块。
像普通的脚本一样,它也包含两种形式,即:内嵌文件、外链文件。
内嵌是这样的:
<script type="module"> |
而外链则是这样的:
<script type="module" src="test.js"></script> |
可以看到,它的用法几乎与普通脚本没多大差别。
5.2 模块加载执行顺序
我们都知道,网页性能优化中有一点,是建议将 JavaScript 代码或者外链文件的 <script>
放置在网页底部。这是因为 JavaScript 会阻塞网页的加载,它需要完成下载 -> 解析 -> 执行 的过程。
如果网页中链接了多个 JavaScript 文件,你是无法得知哪个 JavaScript 文件先下载完,哪个 JavaScript 先执行。
但是对于没有做异步处理的模块,它们加载与执行的顺序,则取决于它们的所在位置顺序。先来看段代码:
<script type="module" src="test1.js"></script> |
上面代码中,有三个模块,其中两个是外部链接,中间那个是内联模块。
这三个模块加载的顺序如下:
- 下载并解析
test1.js
- 递归下载并解析在
test1.js
中导入的模块 - 解析行内模块
- 递归下载并解析行内导入的模块
- 下载并解析
test2.js
- 递归下载并解析在
test2.js
中导入的模块
加载完后,接着等待整个文档解析。待文档(document)解析完,于是,开始执行模块。它们的顺序如下:
- 递归执行
test1.js
导入的模块 - 执行
test1.js
- 递归执行行内模块导入的模块
- 执行行内模块
- 递归执行
test2.js
导入的模块 - 执行
test2.js
如果还有更多模块,可以依次类推。我们无需在 <script>
元素上加 defer
属性来延迟模块的下载,因为,带 type="module"
属性的 <script>
本身就自带这种属性的行为。
刚才说到的是没有异步处理的模块,它们是按所在位置先后顺序下载并执行。倘若我们像 <script async src="test.js">
这样,在 <script type="module" src="test.js">
模块元素上加 async
属性,那么,这些模块都将异步下载。如:
<script type="module" async src="test1.js"></script> |
异步加载意味着无需等待文档下载并解析完,模块便能下载执行。正因为如此,我们无法判断,模块 test1.js
与 模块 test2.js
哪一个会先下载并执行。但有一点肯定的是,先下载的模块会优先执行。
以上就是 ES6 中 模块 的相关内容,虽然目前各大主流浏览器都还未支持,但没关系,飘在空中的尘埃,总会有落地的那一天。