(JavaScript)ES6模块与CommonJS模块的异同

前言

ES6模块和CommonJS模块是在JavaScript中用于模块化编程的两种不同标准,本文将用生动形象的例子讲解有关两者的异同

模块化编程简介

模块化编程是一种软件设计和开发方法,旨在将一个大型软件系统分解为小块可维护的模块或组件,这些模块可以独立开发、测试和维护。每个模块通常负责特定功能或任务,它们之间的接口清晰定义,以便实现代码的重用和降低复杂性。模块化编程有以下重要概念和优点:

  1. 封装和抽象:每个模块隐藏了内部实现的细节,只暴露必要的接口,这有助于减少代码的复杂性和提高可维护性。模块的用户只需关心如何使用模块,而不需要了解其内部实现。
  2. 代码重用:模块可以在不同的项目中重复使用,从而提高开发效率。开发人员可以构建和测试单独的模块,然后将它们组合在一起以创建更大的应用程序。
  3. 解耦:模块化编程有助于降低模块之间的耦合度,即模块之间的依赖关系。这使得更容易进行单独的模块测试和维护。
  4. 可维护性:由于每个模块都是相对独立的,因此更容易定位和修复错误。模块化设计有助于减小代码库的规模,使其更易于维护和扩展。
  5. 协作:不同开发人员可以并行开发不同的模块,而不会过多地干扰彼此的工作。这有助于多人团队协作开发大型项目。

在JavaScript中,模块化编程可以通过不同的标准和工具来实现,包括CommonJS、ES6模块、AMD(异步模块定义)等。这些标准提供了不同的语法和机制,但都有助于将JavaScript代码分解为可重用的模块,从而改善了JavaScript应用程序的结构和可维护性。

语法上的差异

ES6模块像一本现代英语教材,而CommonJS模块就像一本古老的字典 ,ES6模块使用了更现代的语法,使用importexport关键字来定义和导出模块。这种语法更加清晰和简洁。相比之下,CommonJS模块使用requiremodule.exports来导入和导出模块,它们的语法更加冗长和古老。

ES6模块:

  • ES6模块采用现代的、更易读的语法。
  • 使用import关键字来导入其他模块中的内容,例如:import { someFunction } from './myModule';
  • 使用export关键字来导出模块中的函数、变量或类,例如:export function someFunction() { /* ... */ }
  • ES6模块支持默认导出和命名导出,可以在同一个模块中混合使用。

CommonJS模块:

  • CommonJS模块采用相对古老的语法。
  • 使用require函数来导入其他模块中的内容,例如:const someFunction = require('./myModule');
  • 使用module.exports对象来导出模块中的内容,例如:module.exports = { someFunction }
  • CommonJS模块通常不直接支持默认导出,但可以通过module.exports设置一个默认导出。

综上所述,ES6模块的语法更现代、更直观,因为它使用importexport关键字,这使得模块导入和导出更清晰。相反,CommonJS模块的语法更加古老,使用requiremodule.exports,这可能显得冗长和不够直观。这些语法差异是由于它们的不同历史和设计目标,ES6模块被设计成更加适合浏览器环境,而CommonJS模块则是为Node.js服务器端环境而创建的。随着时间的推移,ES6模块在JavaScript生态系统中逐渐取代了CommonJS模块,特别是在浏览器端。

动态与静态的不同

ES6模块就像建筑蓝图,而CommonJS模块就像建筑现场。 ES6模块在编译时静态分析,所有导入和导出都在模块加载前确定,这使得代码更加可预测和优化。CommonJS模块在运行时加载,这意味着模块的依赖关系可能在运行时发生变化,不太适合浏览器环境,但在服务器端很有用

ES6模块(静态):

  • ES6模块的依赖关系在编译时 进行静态分析,这意味着在模块加载之前,所有导入和导出关系都已明确定义
  • 在编译时,JavaScript引擎可以确定模块之间的依赖关系,这使得它可以进行更好的优化,例如剔除未使用的导入,从而减小最终构建的文件大小。
  • 静态分析还使代码更加可预测,开发者能够清楚地了解哪些模块被导入,哪些被导出,而不会有意外的行为。

CommonJS模块(动态):

  • CommonJS模块在运行时动态加载,依赖关系只有在代码执行时才会被解析
  • 运行时加载的模块依赖关系可能导致一些问题,特别是在浏览器环境中。例如,如果模块A依赖于模块B,但模块B的加载时间较长,可能会导致模块A在模块B加载之前访问不到它的导出,从而引发错误。
  • 动态加载对于服务器端JavaScript(如Node.js)通常是合适的,因为服务器环境通常是单线程的 ,不会出现上述问题,而且允许延迟加载模块以提高性能

因此,可以将ES6模块视为建筑蓝图,因为它们在编译时已经明确定义了模块之间的依赖关系,而CommonJS模块则类似于建筑现场,因为它们的依赖关系是在运行时动态加载的,这可能会导致一些不确定性和性能问题,尤其在浏览器环境中。这也是为什么ES6模块在现代前端开发中更常用的原因之一。

同步与异步的不同

ES6模块就像按顺序排队购物,而CommonJS模块就像同时抢购。ES6模块是同步加载的,模块的导入和导出是按顺序执行的,不会出现竞态条件。CommonJS模块是异步加载的,模块的加载是并行的,这可能导致竞态条件和不确定性。

ES6模块(同步):

  • ES6模块是同步加载的,这意味着模块的导入和导出是按顺序执行 的,且不会出现竞态条件
  • 当一个模块引用另一个模块时,它会等待被引用的模块完全加载并执行后才继续执行 。这确保了模块之间的依赖关系被满足,代码执行的顺序是可控的

CommonJS模块(异步):

  • CommonJS模块在运行时异步加载,模块的加载是并行的,这可能导致竞态条件和不确定性
  • 当一个模块引用另一个模块时,它会开始加载后立即继续执行,而不等待被引用的模块加载完成。这可能会导致在使用被引用模块的导出之前,尚未加载完成,从而引发错误或不确定的行为。

这种同步与异步的差异是由于两种模块系统的设计目标不同。ES6模块的同步性有助于浏览器在构建时进行更好的优化,而CommonJS模块的异步性有助于服务器端JavaScript的并行加载和性能优化。因此,选择哪种模块系统通常取决于应用程序的需求和运行环境。

ESM的默认导出

ES6模块就像一位有名字的演员,而CommonJS模块就像一位无名的配角。ES6模块支持默认导出,这意味着一个模块可以有一个主要导出,而CommonJS模块通常没有默认导出,每个导出都需要命名。

ES6模块的默认导出:

  • 在ES6模块中,一个模块可以有一个主要的默认导出,这类似于一位有名字的主演。
  • 默认导出是一个特殊的导出方式,用于导出模块中的主要功能、类、或对象。默认导出通常用于模块的核心功能,使其更容易引用和识别。
  • 在导入时,你可以为默认导出起一个名字,但通常不需要使用花括号来指定名称。
js 复制代码
// es6Module.js
const mainFunction = () => {
  // 主要功能的实现
};

export default mainFunction;

然后,你可以这样导入默认导出:

js 复制代码
import myActor from './es6Module';

CommonJS模块的导出:

  • 在CommonJS模块中,通常没有默认导出的概念,而是通过命名导出来实现模块中的功能。
  • 每个导出都需要命名,你需要使用相应的名称来引用导出的功能或对象。
  • 在导入时,你需要使用require和指定的名称来引用模块的导出。
js 复制代码
// commonjsModule.js
const mainFunction = () => {
  // 主要功能的实现
};

exports.mainFunction = mainFunction;

然后,你可以这样导入CommonJS模块的导出:

js 复制代码
const myActor = require('./commonjsModule');
const mainFunction = myActor.mainFunction;

总之,ES6模块支持默认导出,这使得模块的主要功能更容易被引用,就像一位有名字的主演。而CommonJS模块通常没有默认导出,每个导出都需要命名,就像一位无名的配角。这种默认导出的特性在ES6模块中提供了更直观的导入语法和更清晰的模块结构。

CommonJS模块的浅拷贝:

  • 在CommonJS模块中,当一个模块导出一个对象(例如一个JavaScript对象、数组、函数等),其他模块导入它时,实际上获得了一个对该对象的浅拷贝(shallow copy)。
  • 这意味着其他模块获得的对象与原始导出的对象是相同的引用,因此可以修改该对象的属性或成员,这将反映在所有导入该模块的地方。

ES6模块的引用(类似const):

  • 在ES6模块中,当一个模块导出一个对象,其他模块导入它时,它们获得的是一个对原始对象的只读引用,类似于const变量。
  • 这意味着其他模块无法修改导出模块的对象的引用,因为它们获得的引用是只读的,任何尝试修改对象属性的操作都会失败。

这两种方法的区别在于CommonJS模块的导出是基于浅拷贝的,而ES6模块的导出是基于引用的,只读的。这是因为CommonJS模块系统主要是为服务器端设计的,其中通常需要共享可变状态,而ES6模块系统更注重不变性和预测性,特别适用于浏览器环境和前端开发中的模块化编程。

相同之处

CommonJS 和 ES6 Module 都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

关于一些会混淆的点的解释

关于导出模块的对象的修改

ES6模块 中,其他模块导入它后,它们获得的是对原始对象的只读引用 ,类似于const变量。这意味着其他模块可以修改导出模块的对象的属性值,但不能重新分配一个全新的对象给导出模块的标识符。

js 复制代码
// es6Module.js
const sharedObject = { value: 42 };
export { sharedObject };

// anotherES6Module.js
import { sharedObject } from './es6Module';
sharedObject.value = 100; // 修改属性值是允许的

// anotherES6Module.js
import { sharedObject } from './es6Module';
sharedObject = { value: 200 }; // 不能重新分配 sharedObject

在CommonJS模块中,当一个模块导出一个对象,其他模块导入它后,它们获得的是对原始对象的引用,而不是只读引用。这意味着其他模块可以修改导出模块的对象的属性值,也可以重新分配一个全新的对象给导出模块的标识符。因此,在CommonJS模块中,你可以对导出模块的对象的属性值进行更改,也可以更改它的引用。

js 复制代码
// commonjsModule.js
const sharedObject = { value: 42 };
exports.sharedObject = sharedObject;

// anotherCommonJSModule.js
const sharedObject = require('./commonjsModule');
sharedObject.sharedObject.value = 100; // 修改属性值是允许的
sharedObject.sharedObject = { newValue: 200 }; // 重新分配新对象是允许的

在CommonJS模块系统中,模块之间通常共享可变状态,因此其他模块可以更改导出模块的对象的属性值并重新分配新对象。这与ES6模块有所不同,后者更强调不变性和只读性。

关于运行与加载

在服务器端 JavaScript 环境(如 Node.js)通常是单线程的,这意味着 JavaScript 代码在运行时是单线程执行的,即一次只能执行一个操作。这是因为 Node.js 采用了事件驱动的单线程执行模型,它使用事件循环来处理异步操作,但单线程执行 JavaScript 代码。

然而,模块加载的过程可以是并行的。在 Node.js 中,模块加载是基于 CommonJS 模块系统,它允许并行加载模块,特别是在多核处理器的情况下。这意味着多个模块可以同时被加载,而不必等待前一个模块加载完成。这种并行加载是为了提高性能,确保模块加载不会成为性能瓶颈。

在浏览器端,情况可能会更复杂,因为浏览器中的 JavaScript 是运行在单个主线程中的,通常不会有多线程并行执行 JavaScript 代码。但浏览器可以通过异步加载脚本来实现类似的模块并行加载,以避免页面阻塞。

总之,在服务器端 JavaScript 环境中,JavaScript 代码运行时是单线程的,但模块加载可以是并行的。在浏览器端,JavaScript 代码运行时也是单线程的,但通过异步加载和Web Workers等技术,可以实现一定程度的并行加载和执行。

相关推荐
浩瀚地学15 分钟前
【面试算法笔记】0106-数组-区间和
笔记·算法·面试
To_OC8 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC8 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic9 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy9 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志9 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble10 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长11 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯11 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜12 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js