ESM学习笔记

1,428 阅读6分钟

ESM

ESM是ES6引入的JS标准模块化规范,它的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系和输入和输出的变量。

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

特性

  • 编译时确定模块的依赖关系和输入输出变量
  • 自动采用严格模式
  • 一个模块就是一个文件,该模块内部的所有变量,外部无法获取

好处

  • 使得静态分析成了可能,比如类型检验
  • 不再需要UMD模块格式
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

export

如果你希望外部能够读取模块内部的某个变量,必须通过export关键字输出该变量

  • 输出变量
// profile.js
export const a = 1;
export const b = 'aaa';
export const c = 2021;

// 另一种写法
const a = 1;
const b = 'aaa';
const c = 2021;

export {a, b, c}

上面的这两种写法的等价的,但推荐使用第二种写法,因为这样可以在模块的末尾一眼看出该模块输出了哪些变量

  • 输出函数
export function fn() {
    console.log()
}

// 另一种写法
function fn1(){}
function fn2() {}

export {fn1, fn2}
// 给函数重命名
export {fn1 as st1, fn2 as st2, fn2 as st3}

上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

  • 注意:export命令规定的是模块对外的接口,必须与模块内部的变量建立一一对应关系

例如以下的写法会报错

export 1; // 报错
const a = 1;
export a; // 报错

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。

正确的写法

// 写法1
export const a = 1;

// 写法2
const a = 1;
export { a }

// 写法3
const a = 1;
export {a as m}

上面三种写法都是正确的,规定了对外的接口a。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

函数和类同样必须遵守上面的规则

function fn() {}
export fn; // 报错

export function fn() {} // 正确

function fn() {}
export {fn} // 正确
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,获取模块内部实时的值
export let foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面的代码输出时是bar,500ms后会变为baz

  • export只能出现在模块的顶层,不能在块级作用域中

  • export default:用于模块的默认导出,一个模块中只能存在一个export default

// index.js
export default function() {
    ...
}
// 加载,可以为该匿名函数指定任意名字
import aaa from './index.js'

import

import命令用于输入其他模块提供的功能

import { a, b, c } from './index.js';

// 直接使用
console.log(a);
console.log(b);
console.log(c);

import命令也可以给加载的模块重命名

import { a as st1, b as st2, c as st3 } from './index.js';
  • import命令输入的变量都是只读的,因为它本质是输入接口,也就是说,不允许在加载模块的脚本里面,改写接口
import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

import { a } from './xxx.js'

a.foo = 'hello'; // 合法操作
  • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

  • import命令具有提升效果,会提升到整个模块的头部,首先执行。

  • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
  • import语句在没有from的情况下会执行所加载的模块,如果多次执行同一个模块的import,那么只会执行一次
import 'lodash';
import 'lodash';
  • 如果执行的import语句对应的是同一个模块,等同于只加载一次
import { foo } from 'my_module';
import { bar } from 'my_module'; // 不建议这么写

// 等同于
import { foo, bar } from 'my_module';

复合写法

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

export { foo, bar } from 'my_module';

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

import()

ES2020提案 引入import()函数,支持动态加载模块,并且返回一个Promise对象。

import(module)

和CommonJs的差异

  • CommonJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ESM 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

CommonJS模块,都是只能在运行时确定这些东西。

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面的代码实质是整体加载fs模块,生成一个对象,然后从这三个对象上面读取三个方法,这种加载称为运行时加载,因为只有在运行时才能得到这个对象,导致完全无法在编译时做"静态优化"

  • NodeJs从13.2版本开始支持ESM模块

CommonJS 模块加载 ESM 模块

CommonJs不能通过require()命令加载ESM模块,只能通过import()命令加载

(
  async () => {
      await import(modlue)
  }
)()

require()不支持 ESM模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await命令,导致无法被同步加载。

ESM模块加载Commonjs

ESM模块的import命令可以加载CommonJs模块,但只能整体加载,不能单独加载单一的输出项