JavaScript模块化(CommonJS+ES Module )

模块化标准是规范模块定义、导入、导出的规则,确保不同模块之间可正确交互。前端JavaScript的模块化标准经历了从"民间/社区标准"到"官方标准"的发展,目前主流为ES6 Modules,以下是所有主流标准的详细解析。

模块化解决了这些问题:

  • 函数
  • 解决文件分解(全局污染)和聚合(依赖混乱)的问题

一、民间/社区标准(非官方,早期主流)

1. CommonJS(运行时方案)

简介:Node.js默认采用的模块化方案,基于文件定义模块,每个文件就是一个独立模块,最初用于服务器端,后被前端借用(需通过Webpack等工具打包适配浏览器)。

  • commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;
  • 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;
  • exports 和 module.exports 可以负责对模块中的内容进行导出;
  • require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

核心语法(导入/导出):

  • 导出:通过module.exports或exports对象暴露模块成员;

  • 导入:通过require()函数加载模块,同步加载。

代码示例:

javascript 复制代码
// 导出模块(myModule.js)
module.exports = function() {
  console.log('Hello from CommonJS');
};

// 导入模块(index.js)
const myModule = require('./myModule');
myModule();

特点:

  • 同步加载:require()调用时,程序会阻塞,等待模块加载完成,适合服务器端(文件读取速度快,阻塞影响小);

  • 运行时解析:依赖关系在代码执行时动态解析,无法在编译阶段优化;

  • 单例模式:每次require()同一个模块,返回的是同一个实例,避免重复加载。

CommonJS 在编译阶段会对模块代码进行首尾包装,形成独立作用域,解决全局污染问题,包装函数如下:

javascript 复制代码
function wrapper (script) {
    return '(function (exports, require, module, __filename, __dirname) {' + 
        script +
     '\n})'
}
  1. require 核心机制

require是 CommonJS 的核心导入方法,其加载流程、缓存机制、循环引用处理是核心考点。

(1)文件加载流程

require 接收标识符 作为参数,根据标识符类型按优先级查找模块,优先级:缓存 > 核心模块 > 路径文件模块 > 自定义模块

  • 核心模块:Node.js 底层内置(如 fs、path),已编译为二进制,加载速度最快;
  • 路径文件模块 :以./、../、/开头,转换为真实路径后加载;
  • 自定义模块 :第三方库(如 crypto-js),从当前目录node_modules开始向上递归查找,优先读取package.json的 main 属性,无则依次查找index.js/.json/.node
(2)缓存与防重复加载

Node.js 存在Module全局对象,用于缓存已加载的模块,核心逻辑:

  1. 加载模块时先查找Module._cache,有缓存则直接返回module.exports
  2. 无缓存则创建module对象(含exports: {}loaded: false),先加入缓存再执行模块代码;
  3. 执行完成后标记loaded: true,返回module.exports效果:同一模块被多次 require 时,仅执行一次,后续直接读取缓存。
(3)循环引用处理

CommonJS 通过先缓存后执行的机制解决循环引用,核心逻辑:

  1. 模块 A 加载模块 B 时,先将 A 加入缓存,再执行 A 的代码;
  2. A 执行中加载 B,B 加入缓存后执行 B 的代码,B 中再加载 A 时,直接读取 A 的缓存(此时 A 未执行完毕,exports 可能为空 / 部分导出);
  3. B 执行完毕后,A 继续执行剩余代码,补全 exports。注意 :循环引用时,同步上下文可能获取不到模块未执行的导出内容,可通过异步 / 动态加载解决。
(4)动态加载特性

require 是普通函数,可在任意上下文(函数、条件语句)中动态加载模块,按需引入,示例:

javascript 复制代码
// a.js
console.log('我是a文件')
exports.say = function() {
    // 动态加载b.js
    const getMes = require('./b')
    console.log(getMes())
}

2. AMD(Asynchronous Module Definition,异步模块定义)

简介:专门为浏览器端设计的异步模块化标准,解决CommonJS同步加载在浏览器端的阻塞问题(浏览器加载文件需通过网络,同步加载会导致页面卡顿),常与RequireJS(模块加载器)配合使用。

核心语法(导入/导出):

  • 导出:通过define()函数定义模块,可指定依赖;

  • 导入:通过require()函数异步加载模块,加载完成后执行回调函数。

代码示例:

javascript 复制代码
// 定义模块(myModule.js),依赖dependency模块
define(['dependency'], function(dependency) {
  return function() {
    console.log('Hello from AMD');
  };
});

// 加载模块(index.js)
require(['myModule'], function(myModule) {
  myModule();
});

特点:

  • 异步加载:模块加载不阻塞主线程,适合浏览器环境;

  • 提前声明依赖:define()中需提前指定依赖,加载器可并行加载多个依赖模块;

  • 兼容性:需引入RequireJS等加载器,才能在浏览器中运行。

3. UMD(Universal Module Definition,通用模块定义)

简介:一种兼容多模块系统的格式,旨在让同一个模块能在CommonJS、AMD、浏览器全局变量(<script>标签引入)三种环境中无缝运行,解决模块跨环境复用问题。

核心逻辑:通过判断环境(是否支持AMD、CommonJS),自动适配对应的模块规范。

代码示例:

javascript 复制代码
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // 适配AMD环境(如RequireJS)
    define(['b'], factory);
  } else if (typeof exports === 'object') {
    // 适配CommonJS环境(如Node.js)
    module.exports = factory(require('b'));
  } else {
    // 适配浏览器全局变量环境(<script>标签引入)
    root.returnExports = factory(root.b);
  }
}(this, function (b) {
  // 模块核心逻辑
  return {
    greet: function() {
      console.log('Hello from UMD');
    }
  };
}));

二、官方标准(ECMAScript规范,目前主流)

ES6 Module 是JavaScript 官方原生模块化规范 ,从 ES6 开始纳入标准,解决了 CommonJS 仅适用于服务端的问题,核心支持静态编译,是现代前端模块化的首选方案。

1. 核心优势

  • 静态导入导出,支持Tree Shaking(摇树优化,删除未使用代码);
  • 支持import () 动态导入,实现懒加载和代码分割;
  • 原生支持浏览器和服务端(Node.js 14 + 稳定支持);
  • 严格模式运行,避免全局污染。

2. 核心导入导出语法

ES Module 通过export 导出、import 导入,支持命名导出 / 默认导出 / 混合导出

导出方式 示例代码(a.js) 导入方式 示例代码(main.js)
命名导出 export const name = 'xxx';export function say() {} 命名导入 import { name, say } from './a.js';
默认导出 export default { name: 'xxx', author: 'xxx' } 默认导入 import mes from './a.js';(自定义名称)
混合导出 export const name = 'xxx';export default function say() {} 混合导入 import theSay, { name } from './a.js';
重命名导出 export { name as bookName } from './a.js'; 重命名导入 import { name as bookName } from './a.js';
重定向导出 export * from './a.js';(排除 default) - -
仅执行模块 - 仅执行导入 import './a.js';(仅运行,不导出)

3. 核心特性

(1)静态语法特性
  • import/export必须放在模块顶层,不能放在块级作用域、条件语句、函数中;
  • 导入名不能是动态字符串(如import ('default' + name) from './a.js');
  • 编译阶段确定依赖关系,支持静态分析、Tree Shaking、类型检查。
(2)执行特性

ES Module 采用提前加载 + 深度优先遍历 ,执行顺序为子 -> 父 ,与 CommonJS(父 -> 子 -> 父)不同;同时具备缓存机制,同一模块被多次导入时,仅执行一次。

javascript 复制代码
// main.js
console.log('main开始')
import from './a'
import from './b'
console.log('main结束')
// a.js
import from './b'
console.log('a加载')
// b.js
console.log('b加载')
// 执行顺序:b加载 → a加载 → main开始 → main结束
(3)导出绑定特性

ES Module 的导入变量是只读的、与原模块动态绑定的,核心规则:

  1. 导入变量默认为const直接赋值会报错 (如num = 2);
  2. 变量是引用传递,原模块修改变量,导入方会实时同步;
  3. 可通过原模块的方法修改变量,导入方获取最新值。
javascript 复制代码
// a.js
export let num = 1
export const add = () => { num++ }
// main.js
import { num, add } from './a.js'
console.log(num) // 1
add()
console.log(num) // 2
num = 3 // 报错:num is read-only

4. import () 动态导入

为解决 ES Module 静态语法的局限性,提供import () 动态导入特性,是现代前端懒加载的核心实现。

核心特点
  • 函数形式,可在任意上下文中使用(条件、函数);
  • 返回Promise 对象,then 回调中获取模块导出内容;
  • 支持代码分割,按需加载,避免首次加载过大。
javascript 复制代码
// 动态加载b.js
setTimeout(() => {
    import('./b.js').then(res => {
        console.log(res.name) // 命名导出
        console.log(res.default) // 默认导出
    })
}, 0)
实际应用
  • Vue 路由懒加载component: () => import('./Home.vue')
  • React 懒加载React.lazy(() => import('./LazyComponent'))
  • 按需加载业务模块:条件判断加载不同的功能模块。

5. Tree Shaking 实现

Tree Shaking 是 ES Module 的核心优化,依赖静态语法 特性,在编译阶段分析模块导出与导入的引用关系,删除未被使用的代码,减少打包体积。

javascript 复制代码
// a.js:导出3个成员,仅add被使用
export let num = 1
export const add = () => { num++ }
export const del = () => { num-- }
// main.js:仅导入add
import { add } from './a.js'
add()
// 打包结果:仅保留num和add,del被删除

三、CommonJS 与 ES Module 核心对比

对比维度 CommonJS ES Module
标准类型 社区规范,非官方 ES6 官方原生规范
运行环境 主要适用于 Node.js(服务端),前端需打包兼容 浏览器 + Node.js(14+)原生支持
加载方式 同步加载,运行时解析 静态编译(顶层)+ 异步加载(import ()),编译时解析
执行顺序 深度优先,父 -> 子 -> 父 深度优先,子 -> 父
导出方式 单个值导出,最终导出 module.exports 支持多成员命名导出、默认导出
变量绑定 值拷贝,基本类型导出值,引用类型导出地址 动态绑定,引用传递,原模块修改实时同步
导入变量特性 可修改导入的引用类型属性 导入变量只读,不可直接赋值
核心优化 无原生优化,依赖打包工具 支持 Tree Shaking、代码分割(import ())
循环引用处理 先缓存后执行,同步上下文可能获取不到完整导出 静态解析,提前确定依赖,绑定关系实时同步
缓存机制 基于 Module._cache 缓存 module 对象 模块缓存,同一模块仅执行一次
相关推荐
颜酱10 小时前
二分图核心原理与判定算法
javascript·后端·算法
java1234_小锋10 小时前
Java高频面试题:JVM内存为什么要分代?
java·开发语言·jvm
筱砚.10 小时前
C++——lambda
开发语言·c++·算法
两个人的幸福online10 小时前
php开发者 需要 协程吗
android·开发语言·php
sibylyue11 小时前
Typescritpt、ES6
前端·javascript·vue.js
用户30767528112711 小时前
《拒绝卡顿:深入解析 AI 流式 Markdown 的高性能渲染架构》
前端·javascript
Mertens187411 小时前
Zero-Doc:极简的 Spec Coding 落地指南
前端·javascript·ai编程
ZengLiangYi11 小时前
用 1300 行原生 JS 做了一个 Chrome DevTools 扩展,让前后端不再为接口报错截图扯皮
前端·javascript
A_Qyp11 小时前
JeechBoot前端表格内操作设置下拉
前端·javascript
guygg8811 小时前
基于ADMM的MRI-PET高质量图像重建算法MATLAB实现
开发语言·算法·matlab