0%

导出(export)和导入(import)


参考

导出(export)

在声明前导出

通过在声明之前放置 export 来标记任意声明为导出,无论声明的是变量,函数还是类都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 导出数组
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// 导出 const 声明的变量
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// 导出类
export class User {
constructor(name) {
this.name = name;
}
}

ps: 导出 class/function 后没有分号
在类或者函数前的 export 不会让它们变成 函数表达式。尽管被导出了,但它仍然是一个函数声明
大部分 JavaScript 样式指南都不建议在函数和类声明后使用分号

export function sayHi(user) {
alert(`Hello, ${user}!`);
} // 在这里没有分号 ;

导出与声明分开

1
2
3
4
5
6
7
8
9
10
11
12
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}

function sayBye(user) {
alert(`Bye, ${user}!`);
}

export {sayHi, sayBye}; // 导出变量列表

……从技术上讲,我们也可以把 export 放在函数上面。

Export default

在实际中,主要有两种模块。

包含库或函数包的模块,像上面的 say.js。

声明单个实体的模块,例如模块 user.js 仅导出 class User。

大部分情况下,开发者倾向于使用第二种方式,以便每个“东西”都存在于它自己的模块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 📁 user.js
export default class User { // 只需要添加 "default" 即可
constructor(name) {
this.name = name;
}
}
每个文件应该只有一个 export default

……然后将其导入而不需要花括号:

// 📁 main.js
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可

new User('John');

ps: 不用花括号的导入看起来很酷。刚开始使用模块时,一个常见的错误就是忘记写花括号。所以,请记住,import 命名的导出时需要花括号,而 import 默认的导出时不需要花括号



命名的导出导入 | 默认的导出导入
|
export class User {...} | export default class User {...}
|
import {User} from ... | import User from ...

从技术上讲,我们可以在一个模块中同时有默认的导出和命名的导出,但是实际上人们通常不会混合使用它们。模块要么是命名的导出要么是默认的导出。

由于每个文件最多只能有一个默认的导出,因此导出的实体可能没有名称

例如,下面这些都是完全有效的默认的导出
export default class { // 没有类名
constructor() { ... }
}

export default function(user) { // 没有函数名
alert(`Hello, ${user}!`);
}

// 导出单个值,而不使用变量
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

不指定名称是可以的,因为每个文件只有一个 export default,因此不带花括号的 import 知道要导入的内容是什么。

如果没有 default,这样的导出将会出错:

export class { // Error!(非默认的导出需要名称)
constructor() {}
}

“default” 名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
在某些情况下,default 关键词被用于引用默认的导出。

例如,要将函数与其定义分开导出:
function sayHi(user) {
alert(`Hello, ${user}!`);
}

// 就像我们在函数之前添加了 "export default" 一样
export {sayHi as default};


或者,另一种情况,假设模块 user.js 导出了一个主要的默认的导出和一些命名的导出(这种情况很少见,但确实会发生):
// 📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}

export function sayHi(user) {
alert(`Hello, ${user}!`);
}

这是导入默认的导出以及命名的导出的方法:


// 📁 main.js
import {default as User, sayHi} from './user.js';

new User('John');


如果我们将所有东西 * 作为一个对象导入,那么 default 属性正是默认的导出:
// 📁 main.js
import * as user from './user.js';

let User = user.default; // 默认的导出
new User('John');

我应该使用默认的导出吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
命名的导出是明确的。它们确切地命名了它们要导出的内容,因此我们能从它们获得这些信息,这是一件好事。

命名的导出会强制我们使用正确的名称进行导入:
import {User} from './user.js';
// 导入 {MyUser} 不起作用,导入名字必须为 {User}

……对于默认的导出,我们总是在导入时选择名称:
import User from './user.js'; // 有效
import MyUser from './user.js'; // 也有效
// 使用任何名称导入都没有问题

因此,团队成员可能会使用不同的名称来导入相同的内容,这不好。

通常,为了避免这种情况并使代码保持一致,可以遵从这条规则,即导入的变量应与文件名相对应,例如:
import User from './user.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';
...

但是,一些团队仍然认为这是默认的导出的严重缺陷。因此,他们更倾向于始终使用命名的导出。即使只导出一个东西,也仍然使用命名的导出,而不是默认的导出。

这也使得重新导出(见下文)更容易

重新导出 (Re-export)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
“重新导出(Re-export)”语法 export ... from ... 允许导入内容,并立即将其导出(可能是用的是其他的名字),就像这样:
export {sayHi} from './say.js'; // 重新导出 sayHi

export {default as User} from './user.js'; // 重新导出 default

为什么要这样做?我们看一个实际开发中的用例。

想象一下,我们正在编写一个 “package”:一个包含大量模块的文件夹,其中一些功能是导出到外部的(像 NPM 这样的工具允许我们发布和分发这样的 package,但我们不是必须要去使用它们),并且其中一些模块仅仅是供其他 package 中的模块内部使用的 “helpers”。

文件结构可能是这样的:
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...

我们希望通过单个入口暴露包的功能。

换句话说,想要使用我们的包的人,应该只从“主文件” auth/index.js 导入。

像这样:

import {login, logout} from 'auth/index.js'

“主文件”,auth/index.js 导出了我们希望在包中提供的所有功能。

这样做是因为,其他使用我们包的开发者不应该干预其内部结构,不应该搜索我们包的文件夹中的文件。我们只在 auth/index.js 中导出必要的部分,并保持其他内容“不可见”。

由于实际导出的功能分散在 package 中,所以我们可以将它们导入到 auth/index.js,然后再从中导出它们:
// 📁 auth/index.js

// 导入 login/logout 然后立即导出它们
import {login, logout} from './helpers.js';
export {login, logout};

// 将默认导出导入为 User,然后导出它
import User from './user.js';
export {User};
...

现在使用我们 package 的人可以 import {login} from "auth/index.js"

语法 export ... from ... 只是下面这种导入-导出的简写:
// 📁 auth/index.js
// 重新导出 login/logout
export {login, logout} from './helpers.js';

// 将默认导出重新导出为 User
export {default as User} from './user.js';
...

export ... fromimport/export 相比的显着区别是重新导出的模块在当前文件中不可用。所以在上面的 auth/index.js 示例中,我们不能使用重新导出的 login/logout 函数。

ps: 尽管与 import 等效,但以下语法在语法上无效:

import DefaultExport from "bar.js"; // 有效的

export DefaultExport from "bar.js"; // 无效的

这里正确的做法是重命名这个导出:
export { default as DefaultExport } from "bar.js";

Export “as”

1
2
3
4
5
6
7
8
9
10
11
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};

现在 hi 和 bye 是在外面使用时的正式名称

// 📁 main.js
import * as say from './say.js';

say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!

导入(import)

Import *

通常,我们把要导入的东西列在花括号 import {…} 中

1
2
3
4
5
// 📁 main.js
import {sayHi, sayBye} from './say.js';

sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!

但是如果有很多要导入的内容,我们可以使用 import * as 将所有内容导入为一个对象 ( 不建议使用,不方便webpack模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码)

1
2
3
4
5
// 📁 main.js
import * as say from './say.js';

say.sayHi('John');
say.sayBye('John');

Import “as”

使用 as 让导入具有不同的名字。
例如,简洁起见,我们将 sayHi 导入到局部变量 hi,将 sayBye 导入到 bye

1
2
3
4
5
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';

hi('John'); // Hello, John!
bye('John'); // Bye, John!

动态导入

import() 表达式

1
2
3
4
5
6
7
8
9
10
11
下面这样的 import 行不通:
import ... from getModuleName(); // Error, only from "string" is allowed

其次,我们无法根据条件或者在运行时导入:
if(...) {
import ...; // Error, not allowed!
}

{
import ...; // Error, we can't put import in any block
}

如何才能动态地按需导入模块呢?

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。

我们可以在代码中的任意位置动态地使用它。例如

1
2
3
4
5
let modulePath = prompt("Which module to load?");

import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)

或者,如果在异步函数中,我们可以使用 let module = await import(modulePath)。
例如,如果我们有以下模块 say.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 📁 say.js
export function hi() {
alert(`Hello`);
}

export function bye() {
alert(`Bye`);
}

……那么,可以像下面这样进行动态导入:

let {hi, bye} = await import('./say.js');
hi();
bye();

或者,如果 say.js 有默认的导出

1
2
3
4
5
6
7
8
9
10
11
12
13
// 📁 say.js
export default function() {
alert("Module loaded (export default)!");
}

……那么,为了访问它,我们可以使用模块对象的 default 属性:

let obj = await import('./say.js');
let say = obj.default;
// or, in one line: let {default: say} = await import('./say.js');

say();

完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 📁 say.js
export function hi() {
alert(`Hello`);
}

export function bye() {
alert(`Bye`);
}

export default function() {
alert("Module loaded (export default)!");
}

// 📁 index.html

<!doctype html>
<script>
async function load() {
let say = await import('./say.js');
say.hi(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)!
}
</script>
<button onclick="load()">Click me</button>
1

1

1

1

1

1

1

------ The End ------
您的认可是我不断进步的动力!