CommonJS 与 ES6 模块引入的区别详解

1 阅读1分钟

随着 JavaScript 的发展,模块化编程已经成为现代前端开发的基础。目前主流的模块系统有两种:CommonJS 和 ES6 模块。本文将详细对比这两种模块系统的语法、特性和使用场景。

一、CommonJS 模块系统

CommonJS 最初是为了让 JavaScript 能在服务端(如 Node.js)运行而设计的模块规范。

1. 基本语法

导出模块

// 方式一:直接导出单个值
// moduleA.js
const name = 'John';
module.exports = name;
​
// 方式二:导出一个对象
// person.js
const person = { 
  name: 'John', 
  age: 30,
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};
module.exports = person;
​
// 方式三:使用 exports 快捷方式
// utils.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// 等价于:
// module.exports = { add, subtract }

引入模块

// main.js
// 引入单个值
const name = require('./moduleA');
console.log(name); // 'John'// 引入对象
const person = require('./person');
console.log(person.name); // 'John'
console.log(person.age);  // 30
person.greet(); // "Hello, I'm John"// 引入工具函数
const utils = require('./utils');
console.log(utils.add(5, 3)); // 8

2. 核心特性

动态引入

CommonJS 允许在代码运行时动态加载模块:

// 可以根据条件动态引入
if (process.env.NODE_ENV === 'development') {
  const debugModule = require('./debug');
  debugModule.enable();
}
​
// 可以在函数内部引入
function loadModule(moduleName) {
  return require(`./modules/${moduleName}`);
}
​
// 可以在循环中引入
const modules = ['moduleA', 'moduleB', 'moduleC'];
modules.forEach(name => {
  const module = require(`./${name}`);
  module.init();
});

同步加载

CommonJS 的模块加载是同步的:

// 同步加载,代码会等待模块加载完成
const fs = require('fs');        // 核心模块
const express = require('express'); // 第三方模块
const myModule = require('./my-module'); // 本地模块console.log('模块加载完成,继续执行');

值的拷贝

CommonJS 导出的是值的拷贝:

// counter.js
let count = 0;
module.exports = {
  count,
  increment() {
    count += 1;
  },
  getCount() {
    return count;
  }
};
​
// main.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 (仍然是0,因为 count 是原始值的拷贝)
console.log(counter.getCount()); // 1 (需要通过方法获取最新值)

二、ES6 模块系统

ES6 模块是 ECMAScript 2015 中引入的官方模块规范,现已被现代浏览器和 Node.js 支持。

1. 基本语法

导出模块

// 方式一:命名导出(逐个导出)
// person.js
export const name = 'John';
export const age = 30;
export function greet() {
  console.log(`Hello, I'm ${this.name}`);
}
​
// 方式二:批量导出
// utils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
export { add, subtract };
​
// 方式三:默认导出
// math.js
export default class Math {
  static pi = 3.14159;
  static square(x) {
    return x * x;
  }
}
​
// 方式四:混合导出
// shapes.js
export const PI = 3.14159;
export default class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  area() {
    return PI * this.radius ** 2;
  }
}

引入模块

// 引入命名导出
import { name, age, greet } from './person.js';
console.log(name, age);
greet();
​
// 引入并重命名
import { add as addNumbers, subtract } from './utils.js';
​
// 引入默认导出
import Math from './math.js';
console.log(Math.square(4));
​
// 同时引入默认和命名导出
import Circle, { PI } from './shapes.js';
​
// 引入所有导出(命名空间导入)
import * as utils from './utils.js';
console.log(utils.add(5, 3));
console.log(utils.subtract(5, 3));
​
// 只加载模块但不引入任何内容
import './styles.css';

2. 核心特性

静态引入

ES6 模块的引入必须位于顶层,不能动态引入(至少在基础语法上):

// ✅ 正确:顶层引入
import { readFile } from 'fs';
​
// ❌ 错误:不能在条件语句中引入
if (condition) {
  import { readFile } from 'fs'; // 语法错误
}
​
// ❌ 错误:不能在函数中引入
function loadModule() {
  import { readFile } from 'fs'; // 语法错误
}

异步加载

但在实际使用中,可以通过动态 import() 实现异步加载:

// ✅ 动态引入(返回 Promise)
if (condition) {
  import('./heavy-module.js')
    .then(module => {
      module.doSomething();
    })
    .catch(err => {
      console.error('模块加载失败', err);
    });
}
​
// 使用 async/await
async function loadAdminModule() {
  try {
    const adminModule = await import('./admin.js');
    adminModule.init();
  } catch (error) {
    console.error('加载失败', error);
  }
}
​
// 按需加载路由组件(Vue/React 常见用法)
const UserProfile = () => import('./views/UserProfile.vue');

值的引用

ES6 模块导出的是值的引用,导出和导入的变量指向同一块内存:

// counter.js
export let count = 0;
export function increment() {
  count += 1;
}
​
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 (直接更新了)

三、核心区别对比

四、实际应用场景

1. CommonJS 适用场景

// Node.js 服务端应用
const express = require('express');
const mongoose = require('mongoose');
const config = require('./config');
​
// 条件加载不同环境的配置
const env = process.env.NODE_ENV || 'development';
const dbConfig = require(`./config/${env}.js`);
​
// 动态加载插件
function loadPlugin(pluginName) {
  try {
    return require(`./plugins/${pluginName}`);
  } catch (err) {
    console.error(`插件 ${pluginName} 加载失败`);
    return null;
  }
}

2. ES6 模块适用场景

// 现代前端应用(React/Vue 项目)
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
​
// 按需加载(代码分割)
const LazyComponent = React.lazy(() => import('./LazyComponent'));
​
// 明确导入需要的内容,便于 Tree Shaking
import { debounce, throttle } from 'lodash-es';
​
// 类型导入(TypeScript)
import type { User, Product } from './types';

五、混合使用注意事项

在 Node.js 环境中,可以混合使用两种模块系统,但需要注意:

// ES6 模块中引入 CommonJS 模块
import package from 'commonjs-package'; // 默认导入
import { something } from 'commonjs-package'; // 命名导入(有限支持)// CommonJS 中引入 ES6 模块(使用动态 import)
async function loadESModule() {
  const esModule = await import('./es-module.mjs');
  console.log(esModule.default);
}

package.json 配置

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module", // 设置后,.js 文件默认使用 ES6 模块
  
  "exports": {
    ".": {
      "import": "./dist/index.mjs", // ES6 模块入口
      "require": "./dist/index.cjs"  // CommonJS 模块入口
    }
  }
}

总结

  1. CommonJS 适合 Node.js 服务端开发,特别是需要动态加载的场景

  2. ES6 模块 是现代前端开发的标准,支持静态分析和 Tree Shaking

  3. 动态 import() 填补了 ES6 模块的动态加载能力

  4. 实际开发 中,建议新项目优先使用 ES6 模块,可以获得更好的工具支持和性能优化

选择哪种模块系统,应根据项目运行环境、团队习惯和具体需求来决定。