es6模块导入导出
es6 模块化导入导出简介
一、模块化和模块加载方案
模块化指的就是将一个大程序拆分成若干个互相依赖的小文件,然后在用简单的方法拼装起来。
前端工程,在最早的时候是没有模块的概念的。随着前端工程的发展,前端开发也越来越规范化,更像是软件工程了。那么随之而来的,为了解决工程化的问题,就引入了模块的概念。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
当模块化的概念越来越重要的时候,ES6 在语言标准的层面上,实现了模块功能。了解更多。
二、es6 导入导出功能命令
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
2.1 export(导出命令)
一个 JS 文件,可以理解成一个模块,这个模块可以被任意其他的模块引入。文件模块被引入后,所有的东西,都是在自己的作用域中,主动发起引入行为的那个文件,虽然获取到了被引入的对象,但是并不能访问作用域里的东西,所以提供了 export,来决定一个模块对外暴露什么东西。
export 的作用,就是用于从模块中导出函数、对象或原始值,以便其他程序可以通过 import 语句使用它们.
下面介绍下 export 的相关用法。
定义一个模块文件,名为为 moude1.js。里面定义了函数,对象,以及字符串:
1 | function sayHello() { |
上面的 export 关键词可以规定这个文件对外暴露哪些变量。在 export 后的{}内中的变量就是对外暴露的变量可供其他文件使用。所以,在使用 import 导入的文件对象,就不再是一个空对象,而是包含了 export 内容的对象,所以,我们打印出moude1.js
文件对象得到的如图所示:
2.1.1 命名导出的几种写法
第 1 种写法:2.1 的例子中是其中一种写法。
第 2 种写法:每个对外暴露的变量前加上 export
1 | export function sayHello() { |
第 3 种写法:
1 | function sayHello() { |
如果你不想暴露模块当中的变量名字,可以通过 as 来进行操作,上面代码使用as
关键字,重命名了函数sayHello的对外接口。重命名后,sayHello可以用不同的名字输出两次。
注意:需要说明一点的是,export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。及,针对上面的代码来说不能处于 sayHello 函数中,会报错。如:
1 | function sayHello() { |
上面代码报错如下:
2.1.2 默认导出(export default)
export default 文件的默认导出接口。
- 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
- export default 中的 default 是对应的导出接口变量。
- 通过 export 方式导出,在导入时要加{ },export default 则不需要。(在导入的时候会举例说明)
- export default 向外暴露的成员,可以使用任意变量来接收。
1 | function sayHello() { |
2.1.3 取到模块内部实时的值
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
module1.js
1 | export var foo = "bar"; |
2.3 import 命令
使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
比如上面的 module1.js。在其他文件中要使用 module1.js 中暴露的变量,该如何使用呢。
2.3.1 导入整个文件对象
在 main.js 中引入整个文件对象。
1 | import * as m1 from "./moude1.js"; |
示例中,impot 所有(*),使用 as 为文件对象命名为m1
,可以访问到文件对象的所有对外接口。
2.3.2 导入部分接口
在实际开发中,我们并不需要导出所有的接口。我们知道,import 导出的是整个文件对象,那么我们直接在 import
语句中,对这个对象进行解构,就可以获得其中某一部分接口:
1 | import { sayHello, people } from "./moude1.js"; |
上面的语句中,只导入了 moude1.js 中的 sayHello 对象和 people 对象。打印结果如下:
(1**)变量名保持一致**:需要注意的是,导出部分接口的时候,import 大括号里面的变量名,必须与被导入模块(module1.js
)对外接口的暴露对象名一致。
比如,sayHello,people都是**moude1.js*中export{}*中对应的名称。(比如 module1.js 中对外暴露的对象,如下图)
1 | export { sayHello, people, firstName }; |
(2)import
使用 as 关键字:如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6 为提供了重命名的方法,如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,为变量重命名。(比如我们将 moude1.js 中暴露的sayHello 在 main.js 中引入的时候重命名为peopleSayHello)
1 | import { sayHello as moude1Sayhello, people } from "./moude1.js"; |
打印结果如下:
2.3.3 import 命令的几点特点
只读属性:import 导入的变量为常量,所以不允许修改。对于引用类型的变量,变量名不指向数据,而是指向数据所在的地址。故可以修改引用类型的属性的值。常量只是保证变量名指向的地址不变,并不保证该地址的数据不变。
1 | import { a } from "./xxx.js"; |
单例模式:多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。import 同一模块,声明不同接口引用,会声明对应变量,但只执行一次 import 。
1 | import { a } from "./xxx.js"; |
静态执行特性:import 是静态执行,所以不能使用表达式和变量。
1 | import { "f" + "oo" } from "methods"; // error |
2.3.4 导入默认接口
(1)导入带有 export default 的文件对象,module1.js 如下所示
1 | function sayHello() { |
在 main.js 中引入整个文件对象。
1 | import * as m1 from "./moude1.js"; |
打印如下:
可以看出,export default 的作用,是给文件对象添加一个 default 属性,default 属性的值也是一个对象,且和 export default 导出的内容完全一致。
(2)如何只导入默认接口呢?
上面我们说到,本质上,export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任意名字。如下:
1 | import d from "./moude1.js"; // 不需要加{}, 使用任意变量接收 等价为 import {default as d} from "./moude1.js" |
注意:但是这个任意名字不包括**default**。
2.3.5 动态导入
2.3.3 中我们说到 import 是静态执行,所以不能使用表达式和变量。
1 | if (true) { |
引擎处理import
语句是在编译时,这时不会去分析或执行if
语句。所以import
语句放在if
代码块之中毫无意义,因此会报句法错误,而不是执行时错误。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。ES2020 提案 引入import()
函数,支持动态加载模块。
1 | import(specifier); |
上面代码中,import
函数的参数specifier
,指定所要加载的模块的位置。
import()
返回一个 Promise 对象。下面是一个例子:
1 | import("./moude1.js").then((data) => { |
在这段代码中,then 中回调的 data 就是文件模块的整个文件对象(包括 export 和 export default)。
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
适合场合
(1)按需加载。
import()
可以在需要的时候,再加载某个模块。
1 | button.addEventListener("click", (event) => { |
上面代码中,import()
方法放在click
事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
(2)条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。
1 | if (condition) { |
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
(3)动态的模块路径
import()
允许模块路径动态生成。
1 | import(f()) |
上面代码中,根据函数f
的返回结果,加载不同的模块
注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。还是用之前的 moude1.js 举例。
1 | function sayHello() { |
1 | import("./moude1.js").then(({ sayHello, people }) => { |
上面代码中,sayHello
和people
都是myModule.js
的输出接口,可以解构获得。
如果模块有default
输出接口,可以用参数直接获得。
1 | import("./moude1.js").then((myModule) => { |
上面的代码也可以使用具名输入的形式。
1 | import("./myModule.js").then(({ default: theDefault }) => { |
如果想同时加载多个模块,可以采用下面的写法。
1 | Promise.all([ |
import()
也可以用在 async 函数之中。
1 | async function main() { |
2.4 export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
1 | export { foo, bar } from "my_module"; |
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
模块的接口改名和整体输出,也可以采用这种写法。
1 | // 接口改名 |
默认接口的写法如下。
1 | export { default } from "./someModule"; |
具名接口改为默认接口的写法如下。
1 | export { es6 as default } from './someModule'; |
同样地,默认接口也可以改名为具名接口。
1 | export { default as es6 } from "./someModule"; |
ES2020 之前,有一种import
语句,没有对应的复合写法。
1 | import * as someIdentifier from "./someModule"; |
ES2020补上了这个写法。
1 | export * as someIdentifier from "./someModule"; |
2.5 例子
模块 1(module1.js)
1 | let people = { |
模块 2 (module2.js)
1 | let address = "Japan"; |
主引用 (main.js)
1 | import * as m1 from "./moude1.js"; // 导入整个文件对象 |
2.6 试试
问题
答案