
JS 模块化 本质是将复杂的 JS 代码按照功能、职责拆分成独立的文件(模块),每个模块只暴露需要对外提供的接口,同时隔离内部实现,解决全局变量污染、代码复用、依赖管理等问题。
CommonJS (Node)
CommonJS 是一套 Node.js 中默认的模块化规范,也是前端模块化发展中重要的一环,主要用于解决代码的模块化组织和依赖管理问题。
CommonJS 的特点
-
同步加载 :
require()是同步执行的,会阻塞后续代码,直到模块加载完成。适合 Node.js 环境(模块存于本地磁盘,加载速度快),但不适合浏览器(网络加载慢,会阻塞页面渲染)。
-
运行时加载 :模块的导入和导出在代码运行时执行,属于 "动态加载"。
例如,
require()可以写在条件语句中,根据运行时条件动态加载模块。 -
值拷贝:导入的是模块导出值的 "拷贝",若原模块后续修改了导出的基本类型值,导入方不会同步更新(引用类型除外,因拷贝的是引用地址)。
语法 module.exports
module.exports:模块的默认导出对象,本质是一个空对象 {},模块最终导出的内容以 module.exports 为准。
js
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;
js
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 导出 add 和 subtract 函数
module.exports = {
add,
subtract
};
导出单个成员 :若模块只需要导出一个核心功能,可直接给 module.exports 赋值(非对象)。
js
// utils.js
module.exports = (str) => str.toUpperCase(); // 导出一个函数
语法 exports
exports:是 module.exports 的引用(快捷方式),初始时 exports === module.exports。
注意:不能直接给
exports赋值新对象 (会断开与module.exports的引用关系)。
js
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
exports.add = add;
exports.sub = sub;
exports.multiply = (a, b) => a * b;
语法 require
js
console.log("require", require);
require 是一个函数 ,且是 CommonJS 模块系统中最核心的函数 ------ 它是 Node.js 内置的、挂载在每个模块的 module.require 上的函数,用于同步加载并执行其他模块,返回目标模块的 module.exports 对象。
但要注意:
require不是普通函数,它是「函数对象」(兼具函数和对象的特性),还挂载了多个实用属性(如require.resolve、require.cache),是 Node.js 为 CommonJS 模块定制的特殊函数。

js
console.log('module.require', module.require)
在 Node.js 中直接打印 module 对象时,控制台输出里看不到 require 函数,但却能通过 module.require 访问到这个函数 ------ 核心原因是 require 是 module 对象的不可枚举属性 ,默认不会被 console.log 打印出来,但实际存在且可调用。

require 是 Node.js 为开发者封装的 "易用版" 模块加载函数,而 module.require 是更接近底层的 "核心版" 加载函数 ------ 前者挂载了大量辅助开发的属性,后者仅保留最核心的全局状态。
require.resolve
require.resolve 方法用于解析模块标识符对应的绝对路径(只解析路径,不加载模块)。
js
console.log(require.resolve('./utils.js'))
// /Users/xxx/Documents/code/cloudcode/blog/模块化/utils.js
console.log(require.resolve('fs')) // fs
console.log(require.resolve('axios'))
// /Users/xxx/Documents/code/cloudcode/blog/模块化/node_modules/axios/dist/node/axios.cjs
require.resolve.paths
require.resolve.paths 函数用于返回 Node.js 查找模块时的所有候选路径(即「模块查找路径数组」)。
js
console.log(require.resolve.paths('./utils.js'))
console.log(require.resolve.paths('fs'))
console.log(require.resolve.paths('axios'))

require.main
require.main 指向启动当前 Node.js 进程的主模块 (即你执行 node index.js 时的 index.js)。
require.extensions
require.extensions 是 Node.js 用于处理不同后缀模块的加载器映射,键是文件后缀,值是对应的加载函数。
该属性已被 Node.js 废弃(不推荐自定义),但仍保留用于兼容旧代码。
require.cache
require.cache 是 Node.js 模块系统的缓存容器(以模块绝对路径为键,模块对象为值),用于缓存已加载的模块,避免重复加载 / 执行。
模块缓存是 CommonJS 「加载一次,多次复用」的核心(多次
require同一个模块,只会执行一次模块代码);可通过delete require.cache[模块路径]清除指定模块的缓存(常用于热更新场景)。
内置局部对象 module
js
console.log("module", module);
module 是每个 CommonJS 模块的「内置局部对象」(非全局),console.log(module) 打印的内容完整展示了当前模块的元信息、运行状态、依赖关系。
- id,模块的唯一标识。主模块(直接运行的模块)id 为
.;被导入的模块 id 等于filename。 - path,模块所在的目录路径(不含文件名)
- exports,模块的导出对象(核心)。
- loaded,判断模块是否加载 / 执行完成。
- filename,模块的完整绝对路径(包含文件名)
- children,当前模块加载的子模块列表(每个子项都是 Module 实例)
- paths,模块查找第三方依赖的路径列表,Node.js 会按这个顺序查找 node_modules 目录(从当前目录向上递归)。

js
const utils = require("./utils.js");
const fs = require('fs');
console.log("utils.add", utils.add(1, 2));
fs.readFile('./main.js', 'utf-8', (err, data) => {
if (err) {
console.log(err);
return;
}
console.log('main.js content ended');
});
module.exports = {
add: utils.add,
};
console.log("module", module);
console.log("require", require);
js
// utils.js
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;
console.log("utils loading");


ES Module (浏览器、Node)
ES Module(简称 ESM,ES6 模块)是 ECMAScript 2015(ES6)引入的官方官方模块化规范,旨在统一浏览器和 Node.js 的模块化方案。
它通过静态分析(编译时解析依赖)实现更高效的模块管理,支持树摇(Tree-shaking)、循环依赖处理等高级特性,现已成为现代前端开发的主流模块化标准。
相比于早期制定的 CommonJS 规范,ES6的模块化设计有 3 点不同。
- CommonJS 在
运行时完成模块的加载,而 ES6 模块是在编译时完成模块的加载,效率要更高。 - CommonJS 模块是
对象,而 ES6 模块可以是任何数据类型,通过 export 命令指定输出的内容,并通过import命令引入即可。 - CommonJS 模块会在
require 加载时完成执行,而 ES6 的模块是动态引用,只在执行时获取模块中的值。ES6 模块核心的内容在于 export 命令和 import 命令的使用,两者相辅相成,共同为模块化服务。
语法 exports
1、export 的是接口,而不是值
不能直接通过 export 输出变量值,而是需要对外提供接口,必须与模块内部的变量建立一一对应的关系。
js
let obj = {};
let a = 1;
function foo() {}
export obj; // 错误写法
export a; // 错误写法
export foo; // 错误写法
js
let obj = {};
function foo() {}
export let a = 1; // 正确写法
export { obj }; // 正确写法
export { foo }; // 正确写法
2、export 值的实时性
export 对外输出的接口,在外部模块引用时,是实时获取的,并不是 import 那个时刻的值。假如在文件中 export 一个变量,然后通过定时器修改这个变量的值,那么在其他文件中不同时刻使用 import 的变量,值也会不同。
js
// 导出文件export1.js
const name = 'kingx2';
// 一秒后修改变量name的值
setTimeout(() => name = 'kingx3', 1000);
export { name };
// 导入文件import1.js
import { name } from './export1.js';
console.log(name); // kingx2
setTimeout(() => {
console.log(name); // 'kingx3'
}, 1000);
3、使用 as 关键字设置别名如果不想对外暴露内部变量的真实名称,可以使用 as 关键字设置别名,同一个属性可以设置多个别名。
js
const _name = 'kingx';
export {_name as name};
export {_name as name2};
4、相同变量名只能够 export 一次
在同一个文件中,同一个变量名只能够 export 一次,否则会抛出异常。
js
const _name = 'kingx';
const name = 'kingx';
export { _name as name };
export { name }; // 抛出异常,name作为对外输出的变量,只能export一次
5、尽量统一 export
如果文件 export 的内容有很多,建议都放在文件末尾处统一进行export,这样对export的内容能一目了然。
js
const name = 'kingx';
const age = 12;
const sayHello = function () {
console.log('hello');
};
export {
name,
age,
sayHello
};
语法 export default
使用 import 引入的变量名需要和 export 导出的变量名一样。在某些情况下,我们希望不设置变量名也能供 import 使用,import 的变量名由使用方自定义,这时就要使用到export default命令了。
注意:一个文件只有一个
export default语句。import 的内容不需要使用大括号括起来。
js
// export.js
const defaultParam = 1;
export default defaultParam;
// import.js
import param from './export.js';
console.log(param); // 1
语法 import
1、与 export 的变量名相同
import 命令引入的变量需要放在一个大括号里,括成对象的形式,而且 import 的变量名必须与 export 的变量名一致。
js
// export.js
const _name = 'kingx';
export { _name as name };
// import.js
import { _name } from './export.js'; // 抛出异常
import { name } from './export.js'; // 引入正常
2、相同变量名的值只能 import 一次
相同变量名的值只能 import 一次,否则会抛出异常。假如从多个不同的模块中 import 进相同的变量名,则会抛出异常。
js
// export1.js
export const name = 'kingx';
// export2.js
export const name = 'cat';
// 同时从两个模块中引入name变量,会抛出异常。
import {name} from './export1.js';
import {name} from './export2.js'; // 抛出异常
3、import 命令具有提升的效果
import 命令具有提升的效果,会将 import 的内容提升到文件头部。
js
// export.js
export const name = 'kingx';
// import.js
console.log(name); // kingx
import {name} from './export.js';
在上面的代码中,import 语句出现在输出语句的后面,但是仍然能正常输出。本质上是因为 import 是在编译期运行的,在执行输出代码之前已经执行了 import 语句。
4、多次 import 时,只会一次加载
每个模块只加载一次,每个JS文件只执行一次,如果在同一个文件中多次 import 相同的模块,则只会执行一次模块文件,后续直接从内存读取。
js
// export.js
console.log('开始执行');
export const name = 'kingx';
export const age = 12;
// import.js
import {name} from './export.js';
import {age} from './export.js';
在上面的代码中,import 两次 export.js 文件,但是最终只输出了一次"开始执行"。
5、import 的值本身是只读的,不可修改
使用 import 命令导入的值,如果是基本数据类型,那么它们的值是不可以修改的,相当于一个 const 常量;如果是引用数据类型的值,那么它们的引用本身是不能修改的,只能修改引用对应的值本身。
js
// export.js
const obj = {
name: 'kingx5'
};
const age = 15;
export {obj, age};
// import.js
import {obj, age} from './export.js';
obj.name = 'kingx6'; // 修改引用指向的值,正常
obj = {}; // 抛出异常,不可修改引用指向
age = 15; // 抛出异常,不可修改值本身
6、设置引入变量的别名
js
// export1.js
export const name = 'kingx';
// export2.js
export const name = 'cat';
// 使用as关键字设置两个不同的别名,解决了问题
import {name as personName} from './export1.js';
import {name as animalName} from './export2.js';
7、模块整体加载
当我们需要加载整个模块的内容时,可以使用星号(*)配合 as 关键字指定一个对象,通过对象去访问各个输出值。
js
// export.js
const obj = {
name: 'kingx'
};
export const a = 1;
export { obj };
// import.js
import * as a from './export.js';
语法 Dynamic Import 动态导入
js
import(moduleName)
import(moduleName, options)
第一中 ES Module 环境
js
// main.js
function add(a, b) {
return a + b;
}
console.log("main. loading");
const arr = [1, 2, 3];
function getName(arg) {
arr.push(...arg);
}
export { add, arr, getName };
js
import("./main.js")
.then((module) => {
console.log("module", module);
})
.catch((err) => {
console.log("error", err);
});

js
// utils.js
function add(a, b) {
return a + b;
}
console.log("main. loading");
const arr = [1, 2, 3];
function getName(arg) {
arr.push(...arg);
}
export { add, arr, getName };
export default {
getFullName: (firstName, lastName) => `${firstName} ${lastName}`,
}

第二种 CommonJS 环境
js
// utils.js
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;
console.log("utils loading");
js
import("./utils.js")
.then((module) => {
console.log("module", module);
})
.catch((err) => {
console.log("error", err);
});

语法 import.meta
import.meta 是一个只读的全局对象 ,存在于每个 ES Module 模块的顶层作用域中。仅在 ES Module 环境中可用(CommonJS 中打印 import.meta 会直接报错)。
js
console.log('import.meta', import.meta);
import.meta.url 返回当前模块的文件 URL 路径 。 import.meta.resolve异步解析模块路径(替代 require.resolve),返回 Promise。

import attributes 导入属性
import() 导入属性(Import Attributes)是 ES2025(ESNext)的核心新特性,也被称为「Import Assertions 2.0」(导入断言的升级版)。
必须在 ES Module 环境中使用。
Node.js 从 v20.6.0 开始正式支持 ES2025 导入属性(需确保 Node.js 版本 ≥20.6.0)。
导入属性(Import Attributes)功能用于告知运行时应如何加载模块,包括模块解析、获取、解析与执行的行为。它在 import 声明、export...from 声明以及动态导入 import () 中均受支持。
一、静态 import ... with { ... }
静态 import ... with { ... } 中,只能写 type 属性 ,其他属性一律抛错。 type 只支持 type: "json"或type: "webassembly"。
js
import data from "./config.json" with {
type: "json", // 标准属性:指定模块类型为 JSON
};
二、动态 import
js
async function loadJSON() {
const json = await import("./config.json", { with: { type: "json" } });
console.log("loadJSON", json.default);
}
loadJSON();
三、export...from 声明
js
// ✅ 合法:导出 JSON 模块的默认导出,仅声明 type: "json"
// 核心:JSON 模块必须加 with { type: "json" }
export { default as config } from "./config.json" with { type: "json" };
// 也可导出所有内容(命名空间导出)
export * from "./config.json" with { type: "json" };