ES Module学习笔记
📅 2018-11-02 | 🖱️
ECMAScript Modules(ES Modules)是ECMAScript 6(ES6)规范中引入的一项重要特性,于2015年发布。ES6是ECMAScript标准的第六个版本,也被称为ECMAScript 2015。
ES Modules提供了一种新的模块系统,以改进JavaScript中模块化的方式。它引入了import
和export
关键字,允许开发者更方便地组织和管理JavaScript代码。
ES6 标准的发布时间为 2015 年,但由于不同JavaScript引擎的实现进度不同,一些浏览器和环境可能在稍后才完全支持ES Modules。随着时间的推移,大多数现代的浏览器和Node.js 等环境已经广泛支持ES Modules。
1.ES Modules与CommonJS模块化的区别 #
在ES Module之前,JavaScript模块化一直流行的模块化规范有: CommonJS, AMD, CMD,Node.js的模块化之前一直采用CommonJS规范。
ES Modules与CommonJS模块化的不同之处,在语法、加载方式和一些概念上存在一些显著的区别。以下是它们之间的主要不同之处:
语法
- ES Modules: 使用
import
和export
关键字来导入和导出模块。例如:1// 导入模块 2import { someFunction } from './module'; 3 4// 导出模块 5export function myFunction() { 6 // some code 7}
- CommonJS: 使用
require
函数来导入模块,而使用module.exports
或exports
对象来导出模块。例如:1// 导入模块 2const someFunction = require('./module'); 3 4// 导出模块 5module.exports = { myFunction };
- ES Modules: 使用
加载方式
- ES Modules: 支持异步加载,可以通过
import()
函数在运行时动态加载模块。例如:1import('./module') 2 .then((module) => { 3 // 使用导入的模块 4 }) 5 .catch((error) => { 6 // 处理加载错误 7 });
- CommonJS: 通常是同步加载模块的。在运行时动态加载模块的能力相对有限。
- ES Modules: 支持异步加载,可以通过
运行时特性
- ES Modules: 在模块级别上有更严格的作用域,每个模块都是单独的作用域,不会将变量泄漏到全局作用域。默认情况下是严格模式。
- CommonJS: 模块是同步执行的,变量是在模块的顶层定义的,会被添加到模块的
exports
对象上,可能导致变量泄漏到全局作用域。
动态导入
- ES Modules: 支持动态导入,可以在运行时动态决定导入哪个模块。
- CommonJS: 不太适用于动态导入,通常在模块的顶部进行静态导入。
循环依赖:
- ES Modules: 在处理循环依赖时,会创建一个未初始化的引用,直到模块完全加载并执行。
- CommonJS: 允许循环依赖,但可能导致未定义或未初始化的值。
2.开始使用ES Modules #
2.1 基本使用 #
index.html:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Document</title>
7</head>
8<body>
9 <script src="./mod1.js" type="module"></script>
10 <script src="./main.js" type="module"></script>
11</body>
12</html>
mod1.js:
1const sname = 'foo'
2const score = 100
3
4function showMessage() {
5 console.log('hello world')
6}
7
8export {
9 sname,
10 score,
11 showMessage
12}
main.js:
1import { showMessage } from "./mod1.js";
2
3showMessage()
2.2 其他导入导出方式 #
导出时使用别名:
1const sname = 'foo'
2const score = 100
3
4function showMessage() {
5 console.log('hello world')
6}
7
8export {
9 sname as studentName,
10 score,
11 showMessage
12}
定义变量时直接导出:
1export const sname = 'foo'
2export const score = 100
3
4export function showMessage() {
5 console.log('hello world')
6}
导入时使用别名:
1import { showMessage, sname as studentName } from "./mod1.js";
2
3showMessage()
4console.log(studentName)
导入时为整个模块使用别名:
1import * as mod1 from "./mod1.js";
2
3mod1.showMessage()
4console.log(mod1.sname)
2.3 export from #
mod1.js
和 mod2.js
。
mod2.js:
1// 导出一个变量
2export const foo = 'Hello from mod2!';
mod1.js:
1// 导入 mod2.js 中的 foo,并将其重命名为 bar, 同时将bar导出
2export { foo as bar } from './mod2.js';
3
4// 在当前模块中使用重命名后的标识符 bar
5console.log(bar); // 输出:Hello from mod2!
在这个例子中,mod2.js
导出了一个变量 foo
。然后,在 mod1.js
中使用 export { foo as bar } from './mod2.js'
导入了 foo
,并将其重命名为 bar
。最后,通过 console.log(bar)
在当前模块中使用了重命名后的标识符 bar
。
这样的语法对于在当前模块中使用更具描述性的标识符,或者解决命名冲突问题非常有用。
export from相当于export和import的结合使用。export from的使用场景如下:
重新导出模块内容: 当你有一个模块,想要在另一个模块中重新导出其中的一些内容时,可以使用
export from
。这样可以简化导出的过程,而不需要逐一列举每个成员。1// module1.js 2export const a = 1; 3export const b = 2; 4 5// module2.js 6export { a, b } from './module1';
在这个例子中,
module2.js
重新导出了module1.js
中的a
和b
。模块合并: 当你想要将多个模块的内容合并到一个新的模块中时,
export from
也很有用。1// module1.js 2export const a = 1; 3 4// module2.js 5export const b = 2; 6 7// combinedModule.js 8export { a } from './module1'; 9export { b } from './module2';
在这个例子中,
combinedModule.js
合并了module1.js
和module2.js
中的内容。通常在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。模块重命名: 可以使用
as
关键字为导出的成员取一个新的名称。1// module1.js 2export const a = 1; 3 4// module2.js 5export { a as renamedA } from './module1';
在这个例子中,
module2.js
将module1.js
中的a
导出为renamedA
。
export from
提供了一种简便的语法,使得模块的导出和合并更加灵活和清晰。
2.4 export default用法 #
在ES模块中,export default
语法用于导出模块的默认值。一个模块只能有一个默认导出。当其他模块导入这个模块时,可以选择不使用大括号,直接引用默认导出的值。
以下是export default
的用法示例:
在模块中:
1// module.js
2
3// 导出默认值
4const myDefault = 'Default Value';
5export default myDefault;
6
7// 导出其他值
8export const otherValue = 'Other Value';
9export const anotherValue = 'Another Value';
在导入模块的其他地方:
1// importDefault.js
2
3// 导入默认值
4import myDefault1 from './module';
5
6console.log(myDefault1); // 输出: Default Value
在上面的例子中,module.js
模块使用export default
导出了一个默认值 myDefault
。在importDefault.js
模块中,我们可以直接导入并使用这个默认值,而不需要使用大括号。
总结一下:
- 一个模块中只能有一个
export default
export default
默认导出时不需要指定名字- 在导入默认导出的值,不需要使用
{}
,并且可以自定义导入的名字 export default
方便我们与现有的CommonJS等规范相互操作
2.5 import函数 #
在ES模块中,import()
函数是动态导入模块的方式。与静态的import
语句不同,import()
函数允许你在运行时根据条件加载模块,这对于按需加载模块或在特定条件下加载模块非常有用。
基本语法:
1import(modulePath)
2 .then((module) => {
3 // 使用加载的模块
4 })
5 .catch((error) => {
6 // 处理加载模块时的错误
7 });
例子:
1// 普通的静态导入
2import { myFunction } from './myModule';
3
4// 使用import()函数动态导入
5const modulePath = './myModule';
6
7import(modulePath)
8 .then((myModule) => {
9 myModule.myFunction();
10 })
11 .catch((error) => {
12 console.error('模块加载失败:', error);
13 });
主要特点:
返回Promise:
import()
函数返回一个Promise,因此可以使用then
和catch
来处理异步加载的模块。动态路径:
import()
的参数可以是一个包含模块路径的表达式,这使得模块路径可以在运行时动态计算。按需加载: 通过使用
import()
,你可以在需要的时候按需加载模块,而不是一开始就加载所有模块。适用于异步场景: 当需要在异步场景中加载模块时,
import()
是一个有用的工具,例如在异步函数中加载模块。不支持静态分析: 由于
import()
是在运行时执行的,而不是在静态分析阶段,在一些工具中可能无法完全支持静态分析和代码智能提示。
3.ES Module的加载流程 #
《ES modules: A cartoon deep-dive》这篇文章详细介绍了ES Module是如何被浏览器解析并且让模块之间可以相互引用的。
当使用ES Modules进行开发时,实际上以入口文件(如main.js)为根节点创建出一张依赖图。其中不同依赖之间的连接通过import
声明。
这些导入声明就是让浏览器或者Node知道需要加载哪些代码。给定一个文件作为依赖图的入口,浏览器就会按照导入声明来依次加载代码。
但是浏览器不会使用文件本身。它需要将所有的文件解析为一种叫做模块记录Module Records的数据结构才能让浏览器了解文件内容。
然后,模块记录需要转变为模块实例。
ES Module的加载解析流程分为3个阶段:
- 阶段一:构造(Construction),查找、下载并解析所有文件到模块记录中(Module Record)
- 阶段二:实例化(Instantiation),对模块记录进行实例化为模块环境记录,并且分配内存空间,在内存中寻找一块区域来存储所有导出的变量(但还没有填充值)。然后让
export
和import
都指向这些内存块。这个过程叫做链接(linking),解析模块的导入和导出语句,把模块指向对应的内存地址 - 阶段三:求值(Evaluation),运行代码,计算值,并且将值填充到内存地址中
所有的模块,在任何一个阶段都只有一个实例。浏览器会缓存模块实例,这样可以避免当一个模块被多个模块依赖的时候被下载和解析多次。