说说你对ES6模块化的理解?
问题解析
模块化是大型 JavaScript 应用开发的基础。在 ES6 之前,JavaScript 缺乏官方的模块化方案,社区发展出了 CommonJS、AMD、UMD 等多种规范。ES6 引入了原生的模块化系统(ES Modules),为 JavaScript 提供了语言层面的模块支持。
核心概念
什么是模块化?
模块化是将复杂的程序拆分成多个独立的、可复用的代码单元(模块),每个模块有自己的作用域,通过导出(export)和导入(import)与其他模块交互。
ES6 模块的特点
- 静态结构:导入导出在编译时确定,便于静态分析和优化
- 作用域隔离:每个模块有自己的作用域,不会污染全局
- 单例模式:模块只加载执行一次,多次导入返回同一实例
- 严格模式:模块自动运行在严格模式下
详细解答
一、导出(Export)
1. 命名导出(Named Export)
// math.js
// 方式1:单独导出
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
// 方式2:统一导出
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
export { PI, add, subtract };
// 方式3:重命名导出
export { PI as PI_VALUE, add as addNumbers };
2. 默认导出(Default Export)
// utils.js
// 一个模块只能有一个默认导出
export default function greet(name) {
return `Hello, ${name}!`;
}
// 也可以是匿名函数
export default function() {
return 'Anonymous';
}
// 或导出值/对象
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
export default config;
// 默认导出 + 命名导出可以同时存在
export const version = '1.0.0';
export default function main() {}
二、导入(Import)
1. 命名导入
// 方式1:导入指定的导出
import { PI, add } from './math.js';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
// 方式2:重命名导入
import { PI as PI_VALUE, add as addNumbers } from './math.js';
console.log(PI_VALUE); // 3.14159
console.log(addNumbers(2, 3)); // 5
// 方式3:导入所有导出
import * as math from './math.js';
console.log(math.PI); // 3.14159
console.log(math.add(2, 3)); // 5
2. 默认导入
// 方式1:命名导入(推荐)
import greet from './utils.js';
console.log(greet('World')); // "Hello, World!"
// 方式2:可以任意命名
import myGreet from './utils.js';
import anything from './utils.js';
// 方式3:同时导入默认和命名导出
import greet, { version } from './utils.js';
// 方式4:将所有导出(包括默认)作为命名空间导入
import * as utils from './utils.js';
console.log(utils.default('World')); // 默认导出
console.log(utils.version); // 命名导出
3. 副作用导入
// 只执行模块,不导入任何值
// 常用于 polyfill 或全局样式
import './polyfill.js';
import './styles.css';
三、动态导入(Dynamic Import)
// 静态导入(编译时确定)
import { add } from './math.js';
// 动态导入(运行时确定)
// 返回 Promise
const math = await import('./math.js');
console.log(math.add(2, 3));
// 条件导入
if (condition) {
const { helper } = await import('./helper.js');
helper();
}
// 动态路径
const moduleName = './module-' + version + '.js';
const module = await import(moduleName);
// 同时导入多个
const [math, utils] = await Promise.all([
import('./math.js'),
import('./utils.js')
]);
深入理解
1. 模块加载机制
// a.js
console.log('a.js 开始执行');
import { foo } from './b.js';
console.log('a.js 结束执行', foo);
// b.js
console.log('b.js 开始执行');
export const foo = 'foo';
console.log('b.js 结束执行');
// 执行顺序:
// 1. "a.js 开始执行"
// 2. "b.js 开始执行"(遇到 import,先执行 b.js)
// 3. "b.js 结束执行"
// 4. "a.js 结束执行 foo"
模块加载特点:
- 模块只执行一次(单例)
- 模块加载是同步的(静态导入)
- 循环引用时,已执行的部分会被导出
2. 循环引用处理
// a.js
import { bar } from './b.js';
export const foo = 'foo';
console.log('a.js:', bar);
// b.js
import { foo } from './a.js';
export const bar = 'bar';
console.log('b.js:', foo);
// main.js
import './a.js';
// 输出:
// b.js: undefined(a.js 还未执行完)
// a.js: bar
3. ES6 模块 vs CommonJS
// ========== CommonJS ==========
// 导出
module.exports = { foo, bar };
exports.foo = foo;
// 导入
const { foo, bar } = require('./module');
const module = require('./module');
// 动态加载
if (condition) {
const module = require('./module');
}
// ========== ES6 模块 ==========
// 导出
export { foo, bar };
export default something;
// 导入
import { foo, bar } from './module.js';
import module from './module.js';
// 动态加载
if (condition) {
const module = await import('./module.js');
}
| 特性 | CommonJS | ES6 模块 |
|---|---|---|
| 加载时机 | 运行时 | 编译时 |
| 加载方式 | 同步 | 静态/动态 |
| 值传递 | 值的拷贝 | 值的引用(只读) |
| 顶层 this | module.exports | undefined |
| 文件扩展名 | .js | .mjs 或 .js(type="module") |
| 树摇优化 | 困难 | 容易 |
4. 模块解析
// 相对路径
import { foo } from './foo.js'; // 同级目录
import { foo } from '../foo.js'; // 上级目录
import { foo } from './lib/foo.js'; // 子目录
// 绝对路径(较少使用)
import { foo } from '/src/foo.js';
// 裸导入(需要导入映射或打包工具)
import { foo } from 'lodash';
import { Component } from 'react';
// URL 导入(现代浏览器支持)
import { foo } from 'https://example.com/module.js';
最佳实践
1. 导出策略
// ✅ 优先使用命名导出
export function helper() {}
export const config = {};
// ✅ 默认导出用于主要功能
export default class MainComponent {}
// ✅ 一个文件一个默认导出 + 多个命名导出
// Button.js
export default function Button() {}
export const ButtonSize = { SMALL: 'sm', LARGE: 'lg' };
export const ButtonType = { PRIMARY: 'primary', DEFAULT: 'default' };
2. 导入组织
// ✅ 按类型分组,顺序:内置/第三方/本地
import fs from 'fs'; // 内置模块
import React from 'react'; // 第三方
import axios from 'axios'; // 第三方
import { helper } from './utils'; // 本地工具
import Button from './components/Button'; // 本地组件
import styles from './styles.css'; // 资源
// ✅ 使用命名空间导入避免命名冲突
import * as userApi from './api/user';
import * as orderApi from './api/order';
userApi.getUser();
orderApi.getOrder();
3. 动态导入优化
// ✅ 路由懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// ✅ 条件加载
async function loadPolyfills() {
if (!('fetch' in window)) {
await import('whatwg-fetch');
}
}
// ✅ 预加载
const button = document.getElementById('load');
button.addEventListener('mouseenter', () => {
import(/* webpackPrefetch: true */ './heavy-module.js');
});
4. 循环引用避免
// ❌ 避免循环引用
// a.js
import { b } from './b.js';
export const a = b + 1;
// b.js
import { a } from './a.js';
export const b = a + 1;
// ✅ 解决方案:提取公共模块
// constants.js
export const BASE_VALUE = 0;
// a.js
import { BASE_VALUE } from './constants.js';
export const a = BASE_VALUE + 1;
// b.js
import { BASE_VALUE } from './constants.js';
export const b = BASE_VALUE + 2;
5. 使用导入映射(Import Maps)
<!-- 在 HTML 中使用 ES 模块 -->
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js",
"lodash/": "https://cdn.jsdelivr.net/npm/lodash-es/"
}
}
</script>
<script type="module">
import { createApp } from 'vue';
import debounce from 'lodash/debounce.js';
const app = createApp({});
</script>
面试要点
-
ES6 模块的特点?
- 静态结构,编译时确定依赖关系
- 自动严格模式
- 模块作用域隔离
- 单例模式,只加载执行一次
-
export 和 export default 的区别?
- export:命名导出,一个模块可以有多个
- export default:默认导出,一个模块只能有一个
- 导入时,命名导出需要用 {},默认导出不需要
-
import 和 import() 的区别?
- import:静态导入,编译时处理,提升执行
- import():动态导入,运行时处理,返回 Promise
- 动态导入支持条件加载和动态路径
-
ES6 模块和 CommonJS 的区别?
- ES6 模块是编译时加载,CommonJS 是运行时加载
- ES6 模块输出值的引用,CommonJS 输出值的拷贝
- ES6 模块支持树摇优化(Tree Shaking)
-
如何处理循环引用?
- 尽量避免循环引用
- 提取公共模块
- 使用动态导入延迟加载
- 重构代码结构
-
什么是 Tree Shaking?
- 消除未使用的代码
- ES6 模块的静态结构使其成为可能
- 打包工具(Webpack、Rollup)会自动进行
-
浏览器如何使用 ES6 模块?
<script type="module">- 文件需要使用 .mjs 扩展名或设置正确的 MIME 类型
- 可以使用导入映射(Import Maps)简化裸导入