es6模块导入导出

es6模块导入导出

es6 模块化导入导出简介

一、模块化和模块加载方案

模块化指的就是将一个大程序拆分成若干个互相依赖的小文件,然后在用简单的方法拼装起来。

前端工程,在最早的时候是没有模块的概念的。随着前端工程的发展,前端开发也越来越规范化,更像是软件工程了。那么随之而来的,为了解决工程化的问题,就引入了模块的概念。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

当模块化的概念越来越重要的时候,ES6 在语言标准的层面上,实现了模块功能。了解更多

二、es6 导入导出功能命令

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

2.1 export(导出命令)

一个 JS 文件,可以理解成一个模块,这个模块可以被任意其他的模块引入。文件模块被引入后,所有的东西,都是在自己的作用域中,主动发起引入行为的那个文件,虽然获取到了被引入的对象,但是并不能访问作用域里的东西,所以提供了 export,来决定一个模块对外暴露什么东西

export 的作用,就是用于从模块中导出函数、对象或原始值,以便其他程序可以通过 import 语句使用它们.

下面介绍下 export 的相关用法。

定义一个模块文件,名为为 moude1.js。里面定义了函数,对象,以及字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
function sayHello() {
console.log("module - 1 : sayHello函数");
}

let people = {
hair: "color",
};

let firstName = "lili";

let lastName = "wang";

export { sayHello, people, firstName };

上面的 export 关键词可以规定这个文件对外暴露哪些变量。在 export 后的{}内中的变量就是对外暴露的变量可供其他文件使用。所以,在使用 import 导入的文件对象,就不再是一个空对象,而是包含了 export 内容的对象,所以,我们打印出moude1.js 文件对象得到的如图所示:

2.1.1 命名导出的几种写法

第 1 种写法:2.1 的例子中是其中一种写法。

第 2 种写法:每个对外暴露的变量前加上 export

1
2
3
4
5
6
7
8
9
10
11
export function sayHello() {
console.log("module - 1 : sayHello函数");
}

export let people = {
hair: "color",
};

export let firstName = "lili";

export let lastName = "wang";

第 3 种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function sayHello() {
console.log("module - 1 : sayHello函数");
}

let people = {
hair: "color",
};

let firstName = "lili";

let lastName = "wang";

export {
people,
firstName,
sayHello as peopleSayHello,
sayHello as liliSayHello,
};

如果你不想暴露模块当中的变量名字,可以通过 as 来进行操作,上面代码使用as关键字,重命名了函数sayHello的对外接口。重命名后,sayHello可以用不同的名字输出两次。

注意:需要说明一点的是,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。及,针对上面的代码来说不能处于 sayHello 函数中,会报错。如:

1
2
3
4
5
6
7
8
9
function sayHello() {
console.log("module - 1 : sayHello函数");
export {
people,
firstName,
sayHello as peopleSayHello,
sayHello as liliSayHello,
};
}

上面代码报错如下:

2.1.2 默认导出(export default)

export default 文件的默认导出接口。

  • 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
  • export default 中的 default 是对应的导出接口变量。
  • 通过 export 方式导出,在导入时要加{ },export default 则不需要。(在导入的时候会举例说明)
  • export default 向外暴露的成员,可以使用任意变量来接收。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sayHello() {
console.log("module - 1 : sayHello函数");
}

let people = {
hair: "color",
};

let firstName = "Michael";

let lastName = "Jackson";

let year = 1958;

export { sayHello, people, firstName };

export default {
name: "default",
};

2.1.3 取到模块内部实时的值

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

module1.js

1
2
export var foo = "bar";
setTimeout(() => (foo = "baz"), 500);

2.3 import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

比如上面的 module1.js。在其他文件中要使用 module1.js 中暴露的变量,该如何使用呢。

2.3.1 导入整个文件对象

在 main.js 中引入整个文件对象。

1
2
3
import * as m1 from "./moude1.js";

console.log(m1);

示例中,impot 所有(*),使用 as 为文件对象命名为m1 ,可以访问到文件对象的所有对外接口。

2.3.2 导入部分接口

在实际开发中,我们并不需要导出所有的接口。我们知道,import 导出的是整个文件对象,那么我们直接在 import 语句中,对这个对象进行解构,就可以获得其中某一部分接口:

1
2
3
4
5
import { sayHello, people } from "./moude1.js";

console.log(sayHello);

console.log(people);

上面的语句中,只导入了 moude1.js 中的 sayHello 对象和 people 对象。打印结果如下:

(1**)变量名保持一致**:需要注意的是,导出部分接口的时候,import 大括号里面的变量名,必须与被导入模块(module1.js)对外接口的暴露对象名一致。

比如,sayHellopeople都是**moude1.js*export{}*中对应的名称。(比如 module1.js 中对外暴露的对象,如下图)

1
export { sayHello, people, firstName };

(2)import使用 as 关键字:如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6 为提供了重命名的方法,如果想为输入的变量重新取一个名字,import命令要使用as关键字,为变量重命名。(比如我们将 moude1.js 中暴露的sayHello 在 main.js 中引入的时候重命名为peopleSayHello

1
2
3
4
5
import { sayHello as moude1Sayhello, people } from "./moude1.js";

console.log(moude1Sayhello);

console.log(people);

打印结果如下:

2.3.3 import 命令的几点特点

只读属性:import 导入的变量为常量,所以不允许修改。对于引用类型的变量,变量名不指向数据,而是指向数据所在的地址。故可以修改引用类型的属性的值。常量只是保证变量名指向的地址不变,并不保证该地址的数据不变。

1
2
3
4
5
import { a } from "./xxx.js";
a = {}; // error

import { a } from "./xxx.js";
a.foo = "hello"; // a = { foo : 'hello' }

单例模式:多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。import 同一模块,声明不同接口引用,会声明对应变量,但只执行一次 import 。

1
2
3
import { a } from "./xxx.js";
import { b } from "./xxx.js";
// 相当于 import { a, b } from "./xxx.js";

静态执行特性:import 是静态执行,所以不能使用表达式和变量。

1
2
3
4
5
6
7
8
9
10
11
import { "f" + "oo" } from "methods"; // error

let module = "methods3";
import { foo } from module; // error

if (true) {
import { foo } from "method1"; // error
} else {
import { foo } from "method2"; // error
}

2.3.4 导入默认接口

(1)导入带有 export default 的文件对象,module1.js 如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sayHello() {
console.log("module - 1 : sayHello函数");
}

let people = {
hair: "color",
};

let firstName = "Michael";

let lastName = "Jackson";

let year = 1958;

export { sayHello, people, firstName };

export default {
name: "default",
};

在 main.js 中引入整个文件对象。

1
2
3
import * as m1 from "./moude1.js";

console.log(m1);

打印如下:

可以看出,export default 的作用,是给文件对象添加一个 default 属性,default 属性的值也是一个对象,且和 export default 导出的内容完全一致。

(2)如何只导入默认接口呢?

上面我们说到,本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。如下:

1
2
3
import d from "./moude1.js"; // 不需要加{}, 使用任意变量接收 等价为 import {default as d} from "./moude1.js"

console.log(d);

注意:但是这个任意名字不包括**default**。

2.3.5 动态导入

2.3.3 中我们说到 import 是静态执行,所以不能使用表达式和变量。

1
2
3
4
5
6
if (true) {
import { foo } from "method1";
} else {
import { foo } from "method2";
}
// error

引擎处理import语句是在编译时,这时不会去分析或执行if语句。所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。ES2020 提案 引入import()函数,支持动态加载模块。

1
import(specifier);

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。

import()返回一个 Promise 对象。下面是一个例子:

1
2
3
import("./moude1.js").then((data) => {
console.log(data);
});

在这段代码中,then 中回调的 data 就是文件模块的整个文件对象(包括 export 和 export default)。

  • import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。
  • import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

适合场合

(1)按需加载。

import()可以在需要的时候,再加载某个模块。

1
2
3
4
5
6
7
8
9
button.addEventListener("click", (event) => {
import("./dialogBox.js")
.then((dialogBox) => {
dialogBox.open();
})
.catch((error) => {
/* Error handling */
});
});

上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

1
2
3
4
5
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}

上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

(3)动态的模块路径

import()允许模块路径动态生成。

1
2
import(f())
.then(...);

上面代码中,根据函数f的返回结果,加载不同的模块

注意点

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。还是用之前的 moude1.js 举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sayHello() {
console.log("module - 1 : sayHello函数");
}

let people = {
hair: "color",
};

let firstName = "Michael";

export { sayHello, people, firstName, foo };

export default {
name: "default",
};
1
2
3
import("./moude1.js").then(({ sayHello, people }) => {
// ...·
});

上面代码中,sayHellopeople都是myModule.js的输出接口,可以解构获得。

如果模块有default输出接口,可以用参数直接获得。

1
2
3
import("./moude1.js").then((myModule) => {
console.log(myModule.default);
});

上面的代码也可以使用具名输入的形式。

1
2
3
import("./myModule.js").then(({ default: theDefault }) => {
console.log(theDefault);
});

如果想同时加载多个模块,可以采用下面的写法。

1
2
3
4
5
6
7
8
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});

import()也可以用在 async 函数之中。

1
2
3
4
5
6
7
8
9
10
async function main() {
const myModule = await import("./myModule.js");
const { export1, export2 } = await import("./myModule.js");
const [module1, module2, module3] = await Promise.all([
import("./module1.js"),
import("./module2.js"),
import("./module3.js"),
]);
}
main();

2.4 export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

1
2
3
4
5
export { foo, bar } from "my_module";

// 可以简单理解为
import { foo, bar } from "my_module";
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的接口改名和整体输出,也可以采用这种写法。

1
2
3
4
5
// 接口改名
export { foo as myFoo } from "my_module";

// 整体输出
export * from "my_module";

默认接口的写法如下。

1
export { default } from "./someModule";

具名接口改为默认接口的写法如下。

1
2
3
4
5
export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

同样地,默认接口也可以改名为具名接口。

1
export { default as es6 } from "./someModule";

ES2020 之前,有一种import语句,没有对应的复合写法。

1
import * as someIdentifier from "./someModule";

ES2020补上了这个写法。

1
2
3
4
5
export * as someIdentifier from "./someModule";

// 等同于
import * as someIdentifier from "./someModule";
export { someIdentifier };

2.5 例子

模块 1(module1.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let people = {
hair: "red",
};

let address = "china";

var foo = "bar";
setTimeout(() => (foo = "baz"), 500);

export function sayHello() {
console.log("module - 1 : sayHello函数");
}

export { people, address, foo as fooDelayedTest };

export default {
name: "default",
};

模块 2 (module2.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let address = "Japan";

export { address };

export default {
name: "default2",
init: function init(params) {
console.log(init);
},
baseInfo: {
number: 0,
length: 10,
},
};

主引用 (main.js)

1
2
3
4
5
6
7
8
9
10
import * as m1 from "./moude1.js"; // 导入整个文件对象
import { sayHello, address } from "./moude1.js"; // 导入部分接口
import { fooDelayedTest } from "./moude1.js"; // 导入export中用as重命名后的接口
import { address as japanAddress } from "./moude2.js"; // 导入address后重命名为peopleAddress
import def from "./moude1.js"; // 导入moude1.js中的默认接口
import def2 from "./moude2.js"; // 导入moude2.js中的默认接口

console.log(address);
console.log(japanAddress);
let { name, init, baseInfo } = def2; // 导出的默认接口可以被解构

2.6 试试

问题

答案

作者

王丽

发布于

2021-01-18

更新于

2021-01-18

许可协议

CC BY-NC-SA 4.0

评论