青蛙小白
博客 / 2018/11

ES Module学习笔记

2018/11/02 · — 字 · 阅读约 — 分钟 ·
目录

ECMAScript Modules(ES Modules)是ECMAScript 6(ES6)规范中引入的一项重要特性,于2015年发布。ES6是ECMAScript标准的第六个版本,也被称为ECMAScript 2015。

ES Modules提供了一种新的模块系统,以改进JavaScript中模块化的方式。它引入了importexport关键字,允许开发者更方便地组织和管理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模块化的不同之处,在语法、加载方式和一些概念上存在一些显著的区别。以下是它们之间的主要不同之处:

  1. 语法

    • ES Modules: 使用 importexport 关键字来导入和导出模块。例如:
      // 导入模块
      import { someFunction } from './module';
      
      // 导出模块
      export function myFunction() {
        // some code
      }
    • CommonJS: 使用 require 函数来导入模块,而使用 module.exportsexports 对象来导出模块。例如:
      // 导入模块
      const someFunction = require('./module');
      
      // 导出模块
      module.exports = { myFunction };
  2. 加载方式

    • ES Modules: 支持异步加载,可以通过 import() 函数在运行时动态加载模块。例如:
      import('./module')
        .then((module) => {
          // 使用导入的模块
        })
        .catch((error) => {
          // 处理加载错误
        });
    • CommonJS: 通常是同步加载模块的。在运行时动态加载模块的能力相对有限。
  3. 运行时特性

    • ES Modules: 在模块级别上有更严格的作用域,每个模块都是单独的作用域,不会将变量泄漏到全局作用域。默认情况下是严格模式。
    • CommonJS: 模块是同步执行的,变量是在模块的顶层定义的,会被添加到模块的 exports 对象上,可能导致变量泄漏到全局作用域。
  4. 动态导入

    • ES Modules: 支持动态导入,可以在运行时动态决定导入哪个模块。
    • CommonJS: 不太适用于动态导入,通常在模块的顶部进行静态导入。
  5. 循环依赖:

    • ES Modules: 在处理循环依赖时,会创建一个未初始化的引用,直到模块完全加载并执行。
    • CommonJS: 允许循环依赖,但可能导致未定义或未初始化的值。

2.开始使用ES Modules

2.1 基本使用

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./mod1.js" type="module"></script>
    <script src="./main.js" type="module"></script>
</body>
</html>

mod1.js:

const sname = 'foo'
const score = 100

function showMessage() {
    console.log('hello world')
}

export {
    sname,
    score,
    showMessage
}

main.js:

import { showMessage } from "./mod1.js";

showMessage()

2.2 其他导入导出方式

导出时使用别名:

const sname = 'foo'
const score = 100

function showMessage() {
    console.log('hello world')
}

export {
    sname as studentName,
    score,
    showMessage
}

定义变量时直接导出:

export const sname = 'foo'
export const score = 100

export function showMessage() {
    console.log('hello world')
}

导入时使用别名:

import { showMessage, sname as studentName } from "./mod1.js";

showMessage()
console.log(studentName)

导入时为整个模块使用别名:

import * as mod1 from "./mod1.js";

mod1.showMessage()
console.log(mod1.sname)

2.3 export from

mod1.jsmod2.js

mod2.js:

// 导出一个变量
export const foo = 'Hello from mod2!';

mod1.js:

// 导入 mod2.js 中的 foo,并将其重命名为 bar, 同时将bar导出
export { foo as bar } from './mod2.js';

// 在当前模块中使用重命名后的标识符 bar
console.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的使用场景如下:

  1. 重新导出模块内容: 当你有一个模块,想要在另一个模块中重新导出其中的一些内容时,可以使用 export from。这样可以简化导出的过程,而不需要逐一列举每个成员。

    // module1.js
    export const a = 1;
    export const b = 2;
    
    // module2.js
    export { a, b } from './module1';

    在这个例子中,module2.js 重新导出了 module1.js 中的 ab

  2. 模块合并: 当你想要将多个模块的内容合并到一个新的模块中时,export from 也很有用。

    // module1.js
    export const a = 1;
    
    // module2.js
    export const b = 2;
    
    // combinedModule.js
    export { a } from './module1';
    export { b } from './module2';

    在这个例子中,combinedModule.js 合并了 module1.jsmodule2.js 中的内容。通常在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。

  3. 模块重命名: 可以使用 as 关键字为导出的成员取一个新的名称。

    // module1.js
    export const a = 1;
    
    // module2.js
    export { a as renamedA } from './module1';

    在这个例子中,module2.jsmodule1.js 中的 a 导出为 renamedA

export from 提供了一种简便的语法,使得模块的导出和合并更加灵活和清晰。

2.4 export default用法

在ES模块中,export default语法用于导出模块的默认值。一个模块只能有一个默认导出。当其他模块导入这个模块时,可以选择不使用大括号,直接引用默认导出的值。

以下是export default的用法示例:

在模块中:

// module.js

// 导出默认值
const myDefault = 'Default Value';
export default myDefault;

// 导出其他值
export const otherValue = 'Other Value';
export const anotherValue = 'Another Value';

在导入模块的其他地方:

// importDefault.js

// 导入默认值
import myDefault1 from './module';

console.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()函数允许你在运行时根据条件加载模块,这对于按需加载模块或在特定条件下加载模块非常有用。

基本语法:

import(modulePath)
  .then((module) => {
    // 使用加载的模块
  })
  .catch((error) => {
    // 处理加载模块时的错误
  });

例子:

// 普通的静态导入
import { myFunction } from './myModule';

// 使用import()函数动态导入
const modulePath = './myModule';

import(modulePath)
  .then((myModule) => {
    myModule.myFunction();
  })
  .catch((error) => {
    console.error('模块加载失败:', error);
  });

主要特点:

  1. 返回Promise: import()函数返回一个Promise,因此可以使用thencatch来处理异步加载的模块。

  2. 动态路径: import()的参数可以是一个包含模块路径的表达式,这使得模块路径可以在运行时动态计算。

  3. 按需加载: 通过使用import(),你可以在需要的时候按需加载模块,而不是一开始就加载所有模块。

  4. 适用于异步场景: 当需要在异步场景中加载模块时,import()是一个有用的工具,例如在异步函数中加载模块。

  5. 不支持静态分析: 由于import()是在运行时执行的,而不是在静态分析阶段,在一些工具中可能无法完全支持静态分析和代码智能提示。

3.ES Module的加载流程

《ES modules: A cartoon deep-dive》这篇文章详细介绍了ES Module是如何被浏览器解析并且让模块之间可以相互引用的。

当使用ES Modules进行开发时,实际上以入口文件(如main.js)为根节点创建出一张依赖图。其中不同依赖之间的连接通过import声明。

04_import_graph-500x291.png
04_import_graph-500x291.png

这些导入声明就是让浏览器或者Node知道需要加载哪些代码。给定一个文件作为依赖图的入口,浏览器就会按照导入声明来依次加载代码。

但是浏览器不会使用文件本身。它需要将所有的文件解析为一种叫做模块记录Module Records的数据结构才能让浏览器了解文件内容。

hicc_me-270170697_05_module_record-768x441.png
hicc_me-270170697_05_module_record-768x441.png

然后,模块记录需要转变为模块实例。

ES Module的加载解析流程分为3个阶段:

  • 阶段一:构造(Construction),查找、下载并解析所有文件到模块记录中(Module Record)
  • 阶段二:实例化(Instantiation),对模块记录进行实例化为模块环境记录,并且分配内存空间,在内存中寻找一块区域来存储所有导出的变量(但还没有填充值)。然后让exportimport都指向这些内存块。这个过程叫做链接(linking),解析模块的导入和导出语句,把模块指向对应的内存地址
  • 阶段三:求值(Evaluation),运行代码,计算值,并且将值填充到内存地址中

07_3_phases.png
07_3_phases.png

所有的模块,在任何一个阶段都只有一个实例。浏览器会缓存模块实例,这样可以避免当一个模块被多个模块依赖的时候被下载和解析多次。

参考

TAGS # es6
评论