JavaScript|ES6模块化

JS中的模块其实就是多个代码的组织形式,是JS在ES6开始正式从语言级别层面支持模块的语法

一个模块就是一个文件,一个脚本就是一个模块

  • export 关键字标记了可以从当前模块外部访问的变量和函数。
  • import 关键字允许从其他模块导入功能。

模块核心功能

严格模式运行

模块始终在严格模式下运行,对于一个未声明的变量进行赋值将会报错,例如:

1
2
3
<script type="module">
a = 5; // error
</script>

模块级作用域

1
2
3
4
5
6
export let user = "John"
import {user} from './user.js';

document.body.innerHTML = user;
<!doctype html>
<script type="module" src="hello.js"></script>

需要注意的是:在浏览器中,对于 HTML 页面,每个 <script type="module"> 都存在独立的顶级作用域,下面这种就会报错

1
2
3
4
5
6
7
8
<script type="module">
// 变量仅在这个 module script 内可见
let user = "John";
</script>

<script type="module">
alert(user); // Error: user is not defined
</script>

模块代码只在第一次导入时被解析执行

1
2
3
4
5
6
7
8
9
10
11
12
13
export let admin = {
name : "1"
};
// 📁 1.js
import { admin } from './admin.js';
admin.name = "Pete";

// 📁 2.js
import { admin } from './admin.js';
alert(admin.name); // Pete

// 1.js 和 2.js 引用的是同一个 admin 对象
// 在 1.js 中对对象做的更改,在 2.js 中也是可见的

该admin.js模块只执行了一次。生成导出admin对象,然后这些导出在1和2的导入之间共享,因此如果更改了 admin 对象,在其他导入中也会看到。

可以简单的理解为多次导入后,导入的对象具有同一个引用

我们可以借助这种机制来实现比较经典的场景:

  1. 模块导出一些配置方法,例如一个配置对象。
  2. 在第一次导入时,我们对其进行初始化,写入其属性。可以在应用顶级脚本中进行此操作。
  3. 进一步地导入使用模块。
1
2
3
4
5
6
7
8
9
10
export let config = {};

export function sayHi() {
alert(${config.user});
}
import {config} from './admin.js';
config.user = "Pete";
import {sayHi} from './admin.js';

sayHi(); //Pete

导出高阶用法

在声明前导出

我们可以在声明一个变量、函数、类之前进行导出

1
2
3
4
5
6
7
8
9
10
11
12
13
export let months = ['Jan','Feb'];

export const MODULES_NUM = 1;

export function sayHi(){
alter("Hello");
}

export class User {
constructor(name){
this.name=name;
}
}

值得一提的是,上面声明函数之前export进行导出,本质上还是声明了一个函数,而不是一个函数表达式,因此不需要末尾打分号。只有函数表达式,末尾才需要打分号

1
2
3
let sayHi = function(){
alter("Hello");
};

具体函数表达式可以参考函数表达式

导出和声明分开

我们可以先声明,之后再导出,项目代码中比较常见

1
2
3
4
5
6
7
8
9
function sayHi(){
alter("1");
}

function sayBye(){
alert("2");
}

export {sayHi, sayBye};

不要使用 import *

明确列出我们需要导入的内容,便于构建工具优化检测

为导出和导入起别名

我们也可以使用 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!

导出也具有类似的语法。

我们将函数导出为 hibye

1
2
3
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};

现在 hibye 是在外面使用时的正式名称:

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

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

export default

为了专注一个模块只做一件事,js提供了export default的语法

每个文件应该只有一个 export default

1
2
3
4
5
6
// 📁 user.js
export default class User {
constructor(name){
this.name = name;
}
}
1
2
3
4
// 📁 main.js
//导入的导出文件中包含default时,不需要{User}进行指定
import User from './user.js'
new User("foo");
命名的导出 默认的导出
export class User {...} export default class User {...}
import {User} from ... import User from ...

一般情况下,我们约定一个模块要么是命名的导出,要么就是默认的导出,一般不会进行混用

重新导出

在包的源码中比较常见,主要的目的是为了将源码中所有的导出内容,统一在index.js中进行声明供外部引用

语法结构是 export ... from ...

1
2
export {default as foo} from './foo.js'
export {bar} from './bar.js'

等价于

1
2
3
4
5
import foo from './foo.js'
export {foo};

import {bar} from './bar.js'
export {bar};

(我们假设foo在foo.js中是默认的导出,即export default foo)

默认导出特殊处理

上面也看到了,对于默认的导出需要一个 default as foo 的处理,

假设我们现在有一个脚本默认导出了一个对象

1
2
3
4
// 📁 user.js
export default class User {
// ...
}

在新的文件中:export User from './user.js'是语法错误

我们必须明确写出 export {default as User} 正如上面的处理那样

因此可以看到,在重新导出时,默认导出需要特殊处理,因此很多开发者也不喜欢默认导出,统一使用命名导出的方式


JavaScript|ES6模块化
http://example.com/2025/02/19/JavaScript-ES6模块化/
作者
Noctis64
发布于
2025年2月19日
许可协议