⚡ 编程实验室🏗️ HTML🎨 CSS⚡ JavaScript🐍 Python🗄️ SQL☕ Java⚛️ React💚 Vue🟢 Node.js⚙️ C语言🐘 PHP🐹 Go🔷 TypeScript🐬 MySQL🔧 C++🎯 C#🦀 Rust🅱️ Bootstrap💡 jQuery🎸 Django🍃 MongoDB👗 Sass🎪 Kotlin📊 R语言📋 XML📊 Excel🐘 PostgreSQL🐳 Docker🅰️ Angular🎮 游戏🏠 网站首页

JS 模块化

学习 import/export 组织代码 · 难度:进阶 · +15XP

JavaScript 模块化编程

随着 Web 应用规模不断增长,将所有代码写在一个文件中变得完全不可维护。模块化(Modularization)让我们可以将代码拆分为独立、可复用的文件,每个文件专注于一个功能领域。JavaScript 的模块系统经历了从无到有、从民间方案到官方标准的演进,最终在 ES6(ECMAScript 2015)中确立了官方标准:ES Modules(ESM)

没有模块时的混乱局面

在模块系统出现之前,所有 JavaScript 文件通过多个 <script> 标签加载到同一个 HTML 页面中。所有变量和函数都挂载在全局作用域下,不同的 js 文件之间很容易发生命名冲突,而且脚本的加载顺序直接影响功能是否正常,管理起来十分痛苦。

// ===== 没有模块时的问题 =====
// file1.js
var appName = 'MyApp';
var version = '1.0.0';
function init() {
  console.log('初始化应用...');
}

// file2.js (同一个页面中) var appName = 'YourApp'; // 无意中覆盖了 file1.js 的变量! function init() { // 覆盖了 file1.js 的 init 函数! console.log('你的应用已启动'); }

// 加载顺序决定最终结果,还要维护几十个 script 标签的顺序! // 这种全局污染是大型项目中 bug 的主要来源

ES6 模块基础 —— export 和 import

ES6 引入了官方模块标准。一个 .js 文件就是一个独立的模块,模块内的所有变量和函数默认都是私有的(不会污染全局作用域)。只有通过 export 显式导出的内容,才能被其他模块通过 import 引入使用。模块自动运行在严格模式('use strict')下。

// ===== math.js —— 导出模块 =====
// 命名导出(Named Export)—— 可以导出多个
export const PI = 3.1415926;

export function add(a, b) { return a + b; }

export function multiply(a, b) { return a * b; }

export function circleArea(radius) { return PI * radius * radius; }

// 也可以统一在末尾导出 // export { PI, add, multiply, circleArea };

// 默认导出(Default Export)—— 每个模块只能有一个 // export default function greet(name) { // return 你好,${name}!欢迎来到 JS 模块世界; // }

// ===== app.js —— 导入模块 =====

// 方式一:命名导入(按名称精确导入,名字必须匹配) import { PI, add, multiply, circleArea } from './math.js';

console.log('圆周率:', PI); // 3.1415926 console.log('3 + 5 =', add(3, 5)); // 8 console.log('4 × 7 =', multiply(4, 7)); // 28 console.log('半径5的圆面积:', circleArea(5)); // 约78.54

// 方式二:导入时重命名(避免命名冲突) import { add as mathAdd, multiply as mathMultiply } from './math.js'; console.log(mathAdd(100, 200)); // 300

// 方式三:命名空间导入(将整个模块导入为一个对象) import * as math from './math.js'; console.log(math.PI); console.log(math.add(10, 20)); console.log(math.circleArea(3));

// 方式四:默认导入(可以任意命名) // import greeting from './math.js'; // console.log(greeting('世界'));

// 方式五:混合导入(默认 + 命名) // import greet, { PI, add } from './math.js';

在浏览器中使用 ES Modules

在 HTML 中使用 <script type="module"> 来启用 ES Module 支持。模块脚本默认具有 defer 效果(等 HTML 解析完再执行),且每个模块只执行一次(即使被多次导入)。

<!-- ===== index.html ===== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>ES Modules 实践</title>
</head>
<body>

<!-- 方式一:内联模块脚本 --> <script type="module"> import { add, PI } from './math.js'; console.log('内联模块:', add(1, 2)); console.log('圆周率:', PI); </script>

<!-- 方式二:引入外部模块文件 --> <script type="module" src="app.js"></script>

<!-- 使用 importmap 简化模块路径 --> <script type="importmap"> { "imports": { "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js", "utils/": "/js/utils/" } } </script> <script type="module"> // 现在可以直接用短名称导入 lodash import _ from 'lodash'; console.log(_.chunk([1, 2, 3, 4], 2)); // [[1,2], [3,4]]

// 也可以用路径别名 // import { formatDate } from 'utils/date.js'; </script>

</body> </html>

CommonJS vs ES Modules —— 两大标准全面对比

Node.js 传统上使用 CommonJS(require/module.exports),而现代浏览器和最新 Node.js 使用 ES Modules(import/export)。理解两者的区别对于在不同环境间迁移代码很重要。

特性CommonJSES Modules
语法require() / module.exportsimport / export
加载时机运行时动态加载(同步)编译时静态分析(异步)
输出内容值的拷贝值的只读引用(live binding)
this 指向指向当前模块undefined
默认运行环境Node.js 默认浏览器原生 + Node.js (需 .mjs 或 package.json type:module)
Tree Shaking不支持(打包时无法移除未用代码)支持(静态分析可安全移除未用导出)
动态导入require() 随时可用import() 函数,返回 Promise
循环依赖可能拿到不完整对象通过"引用"机制更好地处理
// ===== CommonJS 写法(Node.js 传统)=====
// math.js
const PI = 3.14159;
function add(a, b) { return a + b; }
module.exports = { PI, add };

// app.js const math = require('./math'); console.log(math.PI); console.log(math.add(1, 2));

// ===== ES Modules 写法(现代标准)===== // math.mjs export const PI = 3.14159; export function add(a, b) { return a + b; }

// app.mjs import { PI, add } from './math.mjs'; console.log(PI); console.log(add(1, 2));

动态导入 —— import() 函数

与静态的 import 声明不同,import() 是一个函数,返回 Promise,可以在代码的任何位置调用。这让我们能够实现按需加载(懒加载)、条件加载和运行时动态决定加载哪个模块。

// 按需懒加载(只在需要时才加载模块,减小初始包体积)
async function loadChartModule() {
  try {
    // 只有调用这个函数时才加载 chart 模块
    let chart = await import('./chart.js');
    chart.renderChart({ type: 'bar', data: [1, 2, 3] });
    return chart;
  } catch (err) {
    console.error('图表模块加载失败:', err);
  }
}
// 用户点击"显示图表"按钮时才调用 loadChartModule()

// 条件加载(根据运行时条件加载不同的模块) let lang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'; let i18n = await import(./locales/${lang}.js); console.log(i18n.greeting); // 根据用户语言显示不同的问候

// 动态导入与 Promise.all 结合 async function loadAllPlugins(pluginNames) { let promises = pluginNames.map(name => import(./plugins/${name}.js) ); let modules = await Promise.all(promises); modules.forEach(mod => mod.init()); console.log('所有插件加载完成'); }

// Promise 风格(不用 async/await) import('./heavy-module.js') .then(module => { module.doSomething(); }) .catch(err => { console.error('模块加载失败:', err); });

  1. 拆分计算器模块:创建 calculator.js,用命名导出提供 add、subtract、multiply、divide 四个函数。创建 app.js 导入并使用这些函数,在 HTML 中通过 type="module" 加载。给每个函数添加输入校验(非数字返回 NaN)。
  2. 用户类模块:创建 user.js,默认导出一个 User 类。类包含 name、email 属性和一个 sayHi() 方法(返回问候语)。在 main.js 中导入 User 类,创建多个实例并调用 sayHi()。再添加一个命名导出 validateEmail(email) 工具函数。
  3. 命名空间与重导出:创建三个模块:stringUtils.js(提供 capitalize、trim)、numberUtils.js(提供 round、randInt)、dateUtils.js(提供 formatDate、daysBetween)。创建 index.js 作为统一入口,用重导出语法将所有工具函数集中暴露。在 app.js 中只需 import { capitalize, round, formatDate } from './utils/index.js' 即可使用所有工具。
  4. 动态导入按需加载:创建两个大型模块:chartRenderer.js(模拟图表渲染,init 时输出"图表模块已加载")和 tableRenderer.js(模拟表格渲染,init 时输出"表格模块已加载")。页面上放置两个按钮,点击时用 import() 动态加载对应模块并调用 init,观察 Network 面板中模块文件的加载时机。
  5. 模块化 Todo 应用:将之前学过的 Todo 应用重构为模块化结构:todoModel.js(数据管理:添加、删除、切换完成状态)、todoView.js(DOM 渲染:根据数据更新 HTML)、todoController.js(事件处理:绑定按钮和输入框事件,协调 Model 和 View)、app.js(入口文件,初始化控制器)。使用 ES Modules 的 import/export 连接各模块。
Ctrl+Enter
🚀 升级VIP
解锁全部课程+AI助手

🏆 学习排行

加载中...

📊 统计

📖 231 篇
0 完成
🔥 0