返回首页

说说你对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>

面试要点

  1. ES6 模块的特点?

    • 静态结构,编译时确定依赖关系
    • 自动严格模式
    • 模块作用域隔离
    • 单例模式,只加载执行一次
  2. export 和 export default 的区别?

    • export:命名导出,一个模块可以有多个
    • export default:默认导出,一个模块只能有一个
    • 导入时,命名导出需要用 {},默认导出不需要
  3. import 和 import() 的区别?

    • import:静态导入,编译时处理,提升执行
    • import():动态导入,运行时处理,返回 Promise
    • 动态导入支持条件加载和动态路径
  4. ES6 模块和 CommonJS 的区别?

    • ES6 模块是编译时加载,CommonJS 是运行时加载
    • ES6 模块输出值的引用,CommonJS 输出值的拷贝
    • ES6 模块支持树摇优化(Tree Shaking)
  5. 如何处理循环引用?

    • 尽量避免循环引用
    • 提取公共模块
    • 使用动态导入延迟加载
    • 重构代码结构
  6. 什么是 Tree Shaking?

    • 消除未使用的代码
    • ES6 模块的静态结构使其成为可能
    • 打包工具(Webpack、Rollup)会自动进行
  7. 浏览器如何使用 ES6 模块?

    • <script type="module">
    • 文件需要使用 .mjs 扩展名或设置正确的 MIME 类型
    • 可以使用导入映射(Import Maps)简化裸导入