一般来说,一个文件就是一个模块,这个文件内的作用域唯一,可以向外暴露变量,函数等。模块化的出现减少了代码的繁琐,利于代码复用和日后维等作用。JavaScript的模块系统有ESM ,CJS, AMD, UMD。
ESM
ECMAScript模块(ESM)是JavaScript的官方标准化模块系统。
基本用法
ECMAScript模块(ESM)是现代JavaScript应用中推荐的模块化标准。以下是ESM的基本用法大纲:
1. 导入模块
使用import
关键字从其他模块导入一个或多个绑定(变量、函数、类等)。
-
导入整个模块:
import * as myModule from '/modules/my-module.js';
-
导入特定的绑定:
import { export1, export2 } from '/modules/my-module.js';
-
导入默认导出:
import defaultExport from '/modules/my-module.js';
2. 导出模块
使用export
关键字将模块内的绑定提供给其他模块使用。
-
导出特定的绑定:
export const export1 = ...; export function export2() { ... };
-
默认导出(每个模块只能有一个):
export default function() { ... } // 或者 const myDefault = ...; export default myDefault;
-
导出多个绑定:
const name1 = ...; ... const nameN = ...; export { name1, /* …, */ nameN };
3. 重命名导入和导出
可以在导入或导出时重命名绑定。
-
导入时重命名:
import { export1 as alias1 } from '/modules/my-module.js';
-
导出时重命名:
export { myExport as default }; export { variable1 as name1, variable2 as name2, /* …, */ nameN };
4. 导入和导出组合使用
export * from "module-name";
export * as name1 from "module-name";
export { name1, /* …, */ nameN } from "module-name";
export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name";
5. 动态导入
ESM支持通过import()
函数动态导入模块,返回一个Promise对象。
import('/modules/my-module.js')
.then((module) => {
// 使用模块
});
模块解析
-
静态解析: ESM的导入和导出在编译阶段解析,这有助于性能优化和代码分析。
例如,根据 ES6 规范,import 只能在模块顶层声明,所以下面的写法会直接报语法错误,不会有 log 打印,因为它压根就没有进入 执行阶段:
console.log('hello world');
if (true) {
import { resolve } from 'path';
}
// out:
// import { resolve } from 'path';
// ^
// SyntaxError: Unexpected token '{'
加载机制
-
在编译阶段,
import
模块中引入的值就指向了 export 中导出的值。这有点像 linux 中的硬链接,指向同一块磁盘数据。或者拿栈和堆来比喻,这就像两个指针指向了同一个栈。//b.js let b = { c:3 }; let b1 = 4; console.log('running b.mjs'); setTimeout(() => { console.log('b val in b', b, b1) },1000) const setB = (newB, newB1) => { b.c = newB; b1 = newB1 } export { b, b1, setB }
//a.js import { b, b1, setB } from './b.js'; console.log('b val in a', b); console.log('setB to bb'); setB(6,7) // b.c = 6; // b1 =7;
//app.js import './a.js'; import {b, b1} from './b.js'; console.log(b, b1)
-
循环依赖处理: ESM可以更好地处理循环依赖问题。
完整可运行:
//test.js
export class AbstractNode {
constructor(parent) {
this.parent = parent
}
print() {
console.log("this.parent",this.parent)
}
static from(thing, parent) {
// 是否是对象,对象返回node 不是返回leaf
if (thing && typeof thing === 'object') return new Node(parent, thing)
else return new Leaf(parent, thing)
}
}
export class Node extends AbstractNode {
constructor(parent, thing) {
super(parent)
this.children = thing
}
print() {
console.log("node",this.parent,this.children)
}
}
export class Leaf extends AbstractNode {
constructor(parent, value) {
super(parent)
this.value = value
}
print() {
console.log("leaf",this.parent,this.children)
return this.value
}
}
//app.js
import {AbstractNode} from './components/test.js';
let a=AbstractNode.from(1,2);
a.print()
拆分后:
//app.js
import {AbstractNode} from './components/internal.js';
let a=AbstractNode.from(1,2);
a.print()
//internal.js
export * from './ab.js'
export * from './abc.js'
export * from './abcd.js'
//ab.js
import {Node} from './internal.js'
import {Leaf} from './internal.js'
export class AbstractNode {
constructor(parent) {
this.parent = parent
}
print() {
console.log("this.parent",this.parent)
}
static from(thing, parent) {
// 是否是对象,对象返回node 不是返回leaf
if (thing && typeof thing === 'object') return new Node(parent, thing)
else return new Leaf(parent, thing)
}
}
//abc.js
import {AbstractNode} from "./internal.js";
export class Node extends AbstractNode {
constructor(parent, thing) {
super(parent)
this.children = thing
}
print() {
console.log("node",this.parent,this.children)
}
}
//abcd.js
import {AbstractNode} from "./internal.js";
export class Leaf extends AbstractNode {
constructor(parent, value) {
super(parent)
this.value = value
}
print() {
console.log("leaf",this.parent,this.children)
return this.value
}
}
性能
树摇(Tree Shaking): 静态分析允许工具移除未使用的代码,减小最终包的体积。
代码分割(Code Splitting): 可以将代码拆分成多个片段,按需加载。
兼容性和环境
浏览器支持: 现代浏览器原生支持ESM。
Node.js支持: Node.js通过.mjs
扩展名或package.json中的"type": "module"
字段支持ESM。
其他特性
ESM支持在模块顶层使用await关键字。
CJS
module.exports = someValue;
CommonJS(CJS)是一种广泛使用的JavaScript模块规范,主要用于服务器端和桌面应用程序,尤其是Node.js环境中。
基本用法
-
require: 用于导入其他模块。
const moduleA = require('./moduleA');
-
module.exports: 用于导出模块公开的对象或函数。
-
exports:
module.exports
的简写,用于导出多个属性或方法。exports.func1 = function() { /* ... */ }; exports.value = 123;
模块解析
-
CommonJS的模块解析发生在执行阶段,因为 require 和 module 本质上就是个函数或者对象,只有在执行阶段 运行时,这些函数或者对象才会被实例化。因此被称为运行时加载。
加载机制
-
CommonJS 是动态加载,可以在代码的任何地方、任何时间点要求加载模块。
-
对于简单类型,CommonJS 导入的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值。对于引用类型,CommonJS 导入的是被导出的值的引用的拷贝,因此模块内部的变化会影响到这个引用类型值。
// lib.js var counter = 3; function incCounter() { counter++; } let obj = { val: 1 }; const setVal = (newVal) => { obj.val = newVal } module.exports = { counter: counter, incCounter: incCounter, obj, setVal };
// main.js var counter = require('./lib').counter; var incCounter = require('./lib').incCounter; const { obj, setVal } = require('./lib') console.log(counter); // 3 incCounter(); console.log(counter); // 3 console.log(obj); setVal(101); console.log(obj);
-
循环依赖处理:由于 commonjs 可以在任何地方加载模块,所以基本不存在循环依赖问题。 还是上面的例子:
// app.js let AbstractNode = require('./components/ab.js'); let a= AbstractNode.from({a: 1, b: 2},3); a.print()
class AbstractNode { constructor(parent) { this.parent = parent } print() { console.log("this.parent",this.parent) } static from(thing, parent) { let Node=require('./abc.js') let Leaf=require('./abcd.js') // 是否是对象,对象返回node 不是返回leaf if (thing && typeof thing === 'object') return new Node(parent, thing) else return new Leaf(parent, thing) } } module.exports = AbstractNode;
//abc.js let AbstractNode = require("./ab.js"); class Node extends AbstractNode { constructor(parent, thing) { super(parent) this.children = thing } print() { console.log("node",this.parent,this.children) } } module.exports = Node;
//abcd.js let AbstractNode = require("./ab.js"); class Leaf extends AbstractNode { constructor(parent, value) { super(parent) this.value = value } print() { console.log("leaf",this.parent,this.children) return this.value } } module.exports = Leaf;
性能
动态加载的特性使得难以进行静态优化。
兼容性和环境
浏览器支持: 需要使用打包工具(如Webpack或Browserify)来在浏览器中使用。
Node.js支持: Node.js原生支持CJS。 也可以通过明确指定.cjs
扩展名或package.json中的"type": "commonjs"
字段支持CJS。
缺点
由于CommonJS的加载方式是同步加载的,这意味着只有前面执行完成才会继续执行。因为同步就会存在一个问题,加载的速度受到影响。Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD
异步模块定义(Asynchronous Module Definition)是一种用于JavaScript模块的规范,它允许模块和它们的依赖可以被异步加载。这在浏览器环境中特别有用,因为它可以提高页面加载性能,并允许按需加载模块。然而,随着ESM(ECMAScript模块)的普及和原生支持,AMD规范的使用频率有所下降。
基本用法
-
AMD使用
define
函数来定义模块。define
接受模块名、依赖列表和一个定义模块的工厂方法。define('module', ['dep1', 'dep2'], function(dep1, dep2) { // 定义模块的内容 return module; });
-
AMD允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:
define('module', function (require, exports, module){ var someModule = require("someModule"); var anotherModule = require("anotherModule"); someModule.doTehAwesome(); anotherModule.doMoarAwesome(); exports.asplode = function (){ someModule.doTehAwesome(); anotherModule.doMoarAwesome(); }; });
特点
-
异步加载:AMD规范的核心是支持异步加载模块,这意味着可以在不阻塞页面渲染的情况下加载JavaScript资源。
-
按需加载: 可以在程序运行时按需加载模块,而不是在页面加载时一次性加载所有脚本。
-
依赖前置: 模块的依赖在模块代码执行前被加载和解析。
-
浏览器兼容: AMD是为了浏览器环境设计的,但他不是原生的规范,所以在浏览器中使用需要加载器。
加载器
-
RequireJS: 最著名的AMD实现。RequireJS提供了一个优化工具,用于将多个模块打包成一个文件,减少HTTP请求。
-
almond: RequireJS的一个精简版本,用于生产环境,在已知所有依赖项的情况下可以减少文件大小。
UMD
UMD(Universal Module Definition)是一种JavaScript模块的封装格式,它允许JavaScript模块在各种环境中运行,包括客户端(浏览器)和服务器端(如Node.js)。UMD实现了一种模式,能够兼容AMD和CommonJS模块定义方式,同时还支持传统的全局变量方式。这使得UMD成为跨平台共享代码的一种方式。然而,随着ESM(ECMAScript模块)的普及和原生支持,UMD规范的使用频率 也有所下降。
基本用法
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD环境
define(['dependency'], factory);
} else if (typeof exports === 'object') {
// CommonJS环境
module.exports = factory(require('dependency'));
} else {
// 浏览器全局变量环境
root.returnExports = factory(root.dependency);
}
}(this, function (dependency) {
// 模块功能定义
return {};
}));
特点
-
UMD模块可以在AMD和CommonJS环境中无缝工作,同时也支持老式的全局变量方法。
-
UMD模式通常在模块的顶部检测当前环境,根据环境不同采用不同的模块定义方式。
-
当使用 Rollup/Webpack 之类的打包器时,UMD 通常用作备用模块。