JS模块:ESM 与 CJM
跳到导航
跳到搜索
关于[1]
ESM | CJM | |
---|---|---|
应用场景 |
|
|
加载方式 |
|
|
模块机制 |
|
|
循环引用 循环引用 |
|
|
“编译时加载”与“运行时加载”
“编译时加载”:编译时就确定模块的依赖关系,以及导入和导出的接口 “运行时加载”:运行时才确定模块的依赖关系,以及导入和导出的对象
CommonJS 示例:
// 代码合法 const name = getName() console.log(name) import { getName } from 'module.js'
- 由于“运行时加载”,import 将会被提升至代码头部,先于 getName() 执行,所以代码是合法的。
ES Module 示例:
// 代码报错:import 中不能使用表达式 import { 'n' + 'ame' } from 'module.js' // 代码报错:import 中不能使用变量 let module = 'module.js' import { name } from module
- 由于“编译时加载”,import 中不能使用变量、表达式。
“值引用”与“值拷贝”
“值引用”:被导出的绑定值依然可以在本地进行修改 “值拷贝”:一旦值被导出,模块内部的变化就不再影响该值
CommonJS 示例:
- a.js:
var name = 'morrain' var age = 17 exports.name = name exports.age = age exports.setAge = function(a){ age = a }
- b.js:
var a = require('a.js') console.log(a.age) // 输出:18 a.setAge(19) console.log(a.age) // 输出:18
ES Module 示例:
- a.js:
var name = 'morrain' var age = 17 const setAge = a => age = a export { name, age, setAge }
- b.js:
import * as a from 'a.js' console.log(a.age) // 输出:18 a.setAge(19) console.log(a.age) // 输出:19
循环引用
CommonJS:模块在返回时可能尚未完成执行,不会出现无限循环。 ES Module: —— 静态导入:模块在初始化完成之前不能访问 —— 应该避免“循环引用”。 —— 动态导入:由于是异步执行,不会出现无限循环,但可能不能得到正确的内容 —— 应该避免“循环引用”。
CommonJS 示例:
- a.js:
console.log('a starting'); exports.done = false; const b = require('./b.js'); console.log('in a, b.done = %j', b.done); exports.done = true; console.log('a done');
- b.js:
console.log('b starting'); exports.done = false; const a = require('./a.js'); console.log('in b, a.done = %j', a.done); exports.done = true; console.log('b done');
- main.js:
console.log('main starting'); const a = require('./a.js'); const b = require('./b.js'); console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
- 输出:
$ node main.js main starting a starting b starting in b, a.done = false b done in a, b.done = true a done in main, a.done = true, b.done = true
ES Module “静态导入”示例:
- a.mjs:
import b from './b.mjs' let done = false; console.log('a starting'); console.log('in a, b.done = %j', b.done); done = true; console.log('a done'); export default done
- b.mjs:
import a from './a.mjs' let done = false; console.log('b starting'); console.log('in b, a.done = %j', a.done); done = true; console.log('b done'); export default done
- main.mjs:
import a from './a.mjs' import b from './b.mjs' console.log('main starting'); console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
- 输出:
PS D:\Documents\VSCode\helloWorld> node "d:\Documents\VSCode\helloWorld\main.mjs" b starting file:///d:/Documents/VSCode/helloWorld/b.mjs:5 console.log('in b, a.done = %j', a.done); ^ ReferenceError: Cannot access 'a' before initialization at file:///d:/Documents/VSCode/helloWorld/b.mjs:5:34 at ModuleJob.run (node:internal/modules/esm/module_job:194:25) Node.js v18.15.0
ES Module “动态导入”示例:
- a.mjs:
let done = false; console.log('a starting'); async function load() { let b = await import('./b.mjs'); console.log('in a, b.done = %j', b.done); } load() done = true; console.log('a done'); export default done
- b.mjs:
let done = false; console.log('b starting'); async function load() { let a = await import('./a.mjs'); console.log('in b, a.done = %j', a.done); } load() done = true; console.log('b done'); export default done
- main.mjs:
console.log('main starting'); async function load() { let a = await import('./a.mjs'); let b = await import('./b.mjs'); console.log('in main, a.done = %j, b.done = %j', a.done, b.done); } load()
- 输出:
PS D:\Documents\VSCode\helloWorld> node "d:\Documents\VSCode\helloWorld\main.mjs" main starting a starting a done b starting b done in a, b.done = undefined in main, a.done = undefined, b.done = undefined in b, a.done = undefined
ES Module:循环引用的处理
💡 利用“函数提升”解决循环引用 ReferenceError: Cannot access 'a' before initialization
的问题;
示例:
- a.mjs:
import * as b from './b.mjs' console.log('a starting'); console.log(`in a, b.msg1 = ${b.msg1}, b.msg2 = ${b.msg2}, print:${b.print()}`); // var 提升:只会提升声明,不会提升其初始化 var msg1 = 'msg1-a' var msg2 = function(){ return 'msg2-a'; }() // 函数提升 function print(){ return 'hello from a !' } console.log('a done'); export {msg1, msg2, print}
- b.mjs:
import * as a from './a.mjs' console.log('b starting'); console.log(`in b, a.msg1 = ${a.msg1}, a.msg2 = ${a.msg2}, print:${a.print()}`); // var 提升:只会提升声明,不会提升其初始化 var msg1 = 'msg1-b' var msg2 = function(){ return 'msg2-b'; }() // 函数提升 function print(){ return 'hello from b !' } console.log('b done'); export {msg1, msg2, print}
- 运行 a.mjs 输出:
PS D:\Documents\VSCode\demo> node "d:\Documents\VSCode\demo\a.mjs" b starting in b, a.msg1 = undefined, a.msg2 = undefined, print:hello from a ! b done a starting in a, b.msg1 = msg1-b, b.msg2 = msg2-b, print:hello from b ! a done
📜 “变量提升”只能使不报错,但不能得到有效内容
—— 只会提升“声明”,不会提升“初始化” —— 所以,上面变量为 undefined
在 node.js 中[2]
Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。
Node.js 会将以下视为 ES Module:
- 扩展名为
.mjs
的文件。 - 最近的父“package.json”文件中“type”字段值为“module”时,扩展名为
.js
的文件。 - 字符串作为参数传入
--eval
,或通过“STDIN”管道传输到 node,带有标志--input-type=module
。
Node.js 会将以下视为 CommonJS:
- 扩展名为
.cjs
的文件。 - 最近的父“package.json”文件中“type”字段值为“commonjs”时,扩展名为
.js
的文件。 - 字符串作为参数传入
--eval
或--print
,或通过“STDIN”管道传输到 node,带有标志--input-type=commonjs
。
包作者应该设置“type”字段 ! 应避免同时使用 ES Module 和 CommonJS !