居然还有人没搞明白CommonJS 和 ES6 模块到底有什么区别?**来来来,敲黑板一会要考~。我们来详细对比一下 JavaScript 中两种最重要的模块化规范:CommonJSES6 模块(ES Modules, ESM),带你起飞~ 芜湖。

首先模块化是什么?
简单来说,模块化就是把一个复杂的程序按照功能拆分成多个独立的文件。这样做的好处是:避免变量污染(不同文件间的变量不会互相干扰)、方便代码复用(哪里需要就在哪里引入)、易于维护(每个文件只负责一件事)。

CommonJS 和 ES6 模块就是两套不同的“拆文件和组合文件”的规则。

一、核心区别速览

在深入了解之前,可以先看一个直观的对比,有个整体印象:

特性 CommonJS ES6 模块 (ES Modules)
设计初衷 服务器端(如 Node.js)设计 浏览器和服务器设计的官方标准
加载方式 动态(运行时加载) 静态(编译时加载)
加载时机 运行时(代码执行到require才加载) 编译时(代码解析阶段就确定依赖关系)
语法 require() / module.exports import / export
输出值 值的拷贝(基本类型是拷贝,对象是浅拷贝) 值的引用(动态映射,只读但能反映模块内部变化)
异步/同步 同步(适合本地文件读取) 异步(天生支持,适合网络请求)
使用环境 Node.js 默认格式 现代浏览器、Node.js(需配置或使用.mjs
静态分析 不支持(无法在编译时检测错误) 支持(可以进行 Tree Shaking)

二、深入理解 CommonJS

1. 设计哲学

CommonJS 是 Node.js 默认的模块系统。它的设计目标是让 JavaScript 能在服务器端运行。因为模块文件都存在本地硬盘上,读取速度非常快,所以设计成了同步加载

2. 语法与用法
  • 导出:使用 module.exportsexports 对象。

    // 📁 math.js
    const add = (a, b) => a + b;
    const subtract = (a, b) => a - b;
    
    // 方式一:导出整个对象
    module.exports = { add, subtract };
    
    // 方式二:也可以直接给 exports 添加属性(但注意不能直接覆盖 exports 对象)
    // exports.add = add;
    // exports.subtract = subtract;
    
  • 导入:使用 require() 函数。

    // 📁 main.js
    const math = require('./math.js');
    console.log(math.add(2, 3)); // 输出 5
    
3. 关键特性:运行时加载与值的拷贝
  • 运行时加载require 是在代码执行到这一行时,才去读取文件、执行代码、获取 module.exports 的值。因此,require 语句可以写在条件语句里(比如 if 条件满足时才加载模块)。

  • 值的拷贝:对于基本类型(Number, String, Boolean),导入的是这个值的副本。如果在原模块内部修改了这个值,导入的模块是看不到变化的。对于对象,因为是浅拷贝,导入的对象和原模块对象指向同一内存地址。

    // 📁 counter.js
    let count = 0;
    const increment = () => count++;
    module.exports = { count, increment };
    
    // 📁 app.js
    const { count, increment } = require('./counter.js');
    console.log(count); // 输出 0
    increment();
    console.log(count); // 仍然输出 0 (因为 count 是基本类型的拷贝,不是引用)
    

三、深入理解 ES6 模块

1. 设计哲学

ES6 模块是 ECMAScript 官方标准。它被设计成静态的,目的是让 JavaScript 引擎在代码编译阶段就能确定模块的依赖关系,从而进行优化。

2. 语法与用法
  • 导出:使用 export 关键字。

    // 📁 math.js
    // 方式一:逐个导出(命名导出)
    export const add = (a, b) => a + b;
    export const subtract = (a, b) => a - b;
    
    // 方式二:默认导出(一个模块只能有一个默认导出)
    const multiply = (a, b) => a * b;
    export default multiply;
    
  • 导入:使用 import 关键字。

    // 📁 main.js
    // 导入命名导出(名称必须和导出时一致)
    import { add, subtract } from './math.js';
    
    // 导入默认导出(可以自定义名称)
    import multiply from './math.js';
    
    // 或者一次性全部导入
    import * as math from './math.js';
    console.log(math.add(2, 3));
    
3. 关键特性:编译时加载与值的引用
  • 编译时加载(静态执行)import 命令会被 JavaScript 引擎在编译阶段提升到模块顶部进行处理。它不能放在条件语句或函数里。这带来了一个巨大的好处:静态分析。工具可以遍历模块依赖树,打包时移除那些被导入但从未被使用的代码,这就是 Tree Shaking

  • 值的引用(动态绑定)import 导入的是对原始模块内部值的只读引用。这意味着,如果原模块内的值发生了变化,导入模块中读到的值也会随之变化。

    // 📁 counter.js
    export let count = 0;
    export const increment = () => count++;
    
    // 📁 app.js
    import { count, increment } from './counter.js';
    console.log(count); // 输出 0
    increment();
    console.log(count); // 输出 1 (因为是引用,读到了最新的值)
    

四、总结与建议

场景 推荐方案 原因
Node.js 原生开发 CommonJS 默认支持,无需配置。
Node.js 配合 Babel/TypeScript ES6 模块 可以使用更现代的语法,并享受 Tree Shaking 等优化,最终会被转译为 CommonJS 在 Node 中运行。
前端工程化(Vue/React 项目) ES6 模块 现代前端打包工具(Webpack, Vite, Rollup)都基于 ES6 模块进行静态分析和 Tree Shaking,打包出体积更小的代码。
现代浏览器(直接使用) ES6 模块 <script type="module"> 中可以直接使用,浏览器会帮你处理依赖和异步加载。

简单来说,CommonJS 是 Node.js 的“老朋友”,稳定可靠;ES6 模块是整个 JavaScript 生态的未来,它带来的静态优化能力让代码更高效。如果你现在开始一个新的前端项目,ES6 模块无疑是首选。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐