Menu

细说Javascript中的ESM及其与CommonJS区别

esm是什么

esm 是将 javascript 程序拆分成多个单独模块,并能按需导入的标准。和webpackbabel不同的是,esm 是 javascript 的标准功能,在浏览器端和 nodejs 中都已得到实现。使用 esm 的好处是浏览器可以最优化加载模块,比使用库更有效率

esm 标准通过importexport语法实现模块变量的导入和导出

esm 模块的特点

  1. 存在模块作用域,顶层变量都定义在该作用域,外部不可见
  2. 模块脚本自动采用严格模式
  3. 模块顶层的this关键字返回undefined
  4. esm 是编译时加载,也就是只有所有import的模块都加载完成,才会开始执行
  5. 同一个模块如果加载多次,只会执行一次

export

export语句用来导出模块中的变量

// 导出变量
export let count = 1;
export const CONST_VAR = 'CONST_VAR';
// 导出函数
export function incCount() {
    count += 1;
}
// 导出类
export class Demo {

}

function add(x) {
    return x + count;
}
// 使用export导出一组变量
export {
    count,
    add,
    // 使用as重命名导出的变量
    add as addCount,
}

// 导出default
export default add

// 合并导出其他模块的变量
export { name } from './esm_module2.js'
export * from './esm_module2.js'

import

import语句用来导入其他模块的变量

// 导入变量
import { count, incCount, CONST_VAR } from './esm_module1.js';

// 通过as重命名导入的变量
import { addCount as renamedAddCount } from './esm_module1.js';

// 导入默认
import { default as defaultAdd } from './esm_module1.js';
import add from './esm_module1.js';

// 创建模块对象
import * as module1 from './esm_module1.js';

export 导出的是值引用

esm 模块和 commonjs 模块的一个显著差异是,cjs 导出的是值得拷贝,esm 导出的是值的引用。当模块内部的值被修改时,cjs 获取不到被修改后的值,esm 可以获取到被修改后的值。

cjs 例子

// cjs_module1.js
var count = 1;
function incCount() {
    count += 1;
}

module.exports = {
    count: count,
    incCount: incCount,
}

// cjs_demo.js
var { count, incCount } = require('./cjs_module1.js');

console.log(count); // 1
incCount();
console.log(count); // 1

esm 例子

// esm_module1.js
let count = 1;
function incCount() {
    count += 1;
}

export {
    count,
    incCount,
}

// esm_demo.js
import { count, incCount } from './esm_module1.js';

console.log(count); // 1
incCount();
console.log(count); // 2

从实现原理上来看,cjs 的 module.exports是一个对象,在运行期注入模块。在导出语句module.exports.count = count执行时,是给这个对象分配一个count的键,并赋值为1。 这之后模块中的count变量再怎么变化,都不会干扰到module.exports.count

esm 中的export { count }是导出了count变量的一个只读引用,等于说使用者读取count时,值的指向还是模块中count变量的值。

可以看阮一峰的这篇文章

在 html 中使用 esm

使用script标签引入 esm 文件,同时设置type=module,标识这个模块为顶级模块。浏览器将 esm 文件视为模块文件,识别模块的import语句并加载。

<script src="./esm_main.js" type="module"></script>

如果不设置type=module,浏览器认为该文件为普通脚本。检查到文件中存在import语句时,会报如下错误

细说Javascript中的ESM及其与CommonJS区别

esm的加载机制

esm 标准没有规定模块的加载细节,将这些留给具体环境实现。大致上分为下面四个步骤:

解析:实现读取模块的源代码并检查语法错误

加载:递归加载所有import的模块

链接:对每个加载的模块,都生成一个模块作用域,该模块下的所有全局声明都绑定到该作用域上,包括从其他模块导入的内容

运行时:完成所有import的加载和链接,脚本运行每个已经加载的模块中的语句。当运行到全局声明时,什么也不会做(在链接阶段已经将声明绑定到模块作用域)。

可以看下 mdn 上的这篇深入 esm 的文章

动态加载模块

esm 的一个重要特性是编译时加载,这有利于引擎的静态分析。加载的过程会先于代码的执行。却也导致import导入语句不能在函数或者if语句中执行

// 报语法错误
if (true) {
    import add from './esm_module1.js';
}

es2020 提案引入import()函数,用来动态加载模块,并且可以用在函数和if语句中。

import('./esm_module1.js')
  .then(module => {
    console.log(module);
  })

import()函数接受要加载的模块相对路径,返回一个Promise对象,内容是要加载的模块对象。

使用import()函数还可以实现根据变量动态加载模块

async function getTemplate(templateName) {
	let template = await import(`./templates/${templateName}`);
	console.log(template);
}

getTemplate("foo");
getTemplate("bar");
getTemplate("baz");

ESM与CommonJS模块化区别

CommonJS语法

// 导出
module.exports = {}
module.exports.a = xxx
// 导入
var xxx = require('xxx');

ESM语法

// 导出
export const aaa = xxx
export bbb
export default xxx

// 导入
import aaa from './xxx.js'
import {a,b} from './xxx.js'
import * as aaa from './xxx.js'
import { default as defaultAdd } from './esm_module1.js';

两者区别

1.CommonJS 模块输出的是一个值的拷贝,ESM模块输出的是值的引用

当模块内部的值被修改时,cjs 获取不到被修改后的值,而ESM可以。

2.CommonJS 模块是运行时加载,ESM 模块是编译时加载

因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。

而 ESM不是对象,它的对外接口只是一种静态定义,一个“符号连接”,在代码静态解析阶段就会生成。必须写道模块顶部。

es2020 提案引入import()函数,可用来动态加载模块。

3.CommonJS 是同步加载模块,ESM是异步加载模块

CJS 由于运行机制是值拷贝,require会阻塞后续代码。值拷贝完成会缓存其结果。

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。也就是说模块不会缓存运行结果,而是动态地去被加载的模块取值。

4.ESM 引用的模块变量是只读的

由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

5.ESM 同一个模块如果加载多次,只会执行一次

本文固定连接:https://code.zuifengyun.com/2022/01/2448.html,转载须征得作者授权。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持