CommonJS 和 ES6module 的区别

动态与静态

CommonJS 与 ES6 Module 最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段:而“静态”则表示模块依赖关系的建立发生在代码编译阶段。

看一个 CommonJS 的例子:

// calculator.js
module.exports = { name: 'calculator' };
// index.js
const name = require('./calculator.js').name;

模块 A 在加载模块 B 时会执行 B 中的代码,并将其 module.exports 对象作为 require 函数的返回值返回。require的模块路径可以动态指定,支持传入一个表达式,甚至可以通过 if 语句判断是否加载某个模块。因此,在 CommonJS 模块被执行前,我们并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

针对同样的例子,我们再对比看下 ES6 Module 的写法:

// calculator.js
export const name ='calculator';
// index.js
import name from './calculator.js';

ES6 Module 的导入、导出语句都是声明式的,它不支持将表达式作为导入路径,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在语句中)。因此我们说,ES6 Module是一种静态的模块结构,在 ES6 代码的编译阶段就可以分析出模块的依赖关系。它相比 CommonJS 来说具备以下几点优势:

  • 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
  • 模块变量类型检查。JavaScript 属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module 的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
  • 编译器优化。在 CommonJS 等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而 ES6 Module 支持直接导入变量,减少了引用层级,程序效率更高。

值复制与动态映射

在导入一个模块时,对于 CommonJS 来说获取的是一份导出值的副本;而在 ES6 Module 中则是值的动态映射,并且这个映射是只读的。看一个例子,了解一下什么是 CommonJS 中的值复制:

// calculator.js
var count = 0;
module.exports = {count: count,add: function(a,b){count += 1;return a + b;}
}// index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;
console.log(count); // 0(这里的count是calculator.js中count值的副本)
add(2, 3);
console.log(count); // 0(calculator.js中变量值的改变不会对这里的副本造成影响)
count += 1;
console.log(count); // 1(副本的值可以更改)

index.js 中的 count 是 calculator.js 中 count 的一份副本,因此在调用 add 函数时,虽然更改了原本 calculator.js 中 count 的值,但是并不会对 index.js 中导入时创建的副本造成影响。
另一方面,在 CommonJS 中允许对导入的值进行更改。我们可以在 index.js 中更改 countadd ,将其赋予新值。同样,由于是值的副本,这些操作不会影响 calculator.js 本身。
下面使用 ES6 Module 对上面的例子进行改写:

// calculator.js
let count = 0;
const add = function(a, b){count += 1;return a + b;
}
export { count, add }
// index.js
import { count, add } from'./calculator.js';
console.log(count); // 0(对calculator.js中count值的映射)
add(2, 3);
console.log(count); // 1(实时反映calculator.js中count值的变化)
// count += 1; // 不可更改,会抛出SyntaxError:"count”is read-only

上面的例子展示了 ES6 Module 中导入的变量其实是对原有值的动态映射 index.js 中的 count 是对 calculator.js 中 count 值的实时反映,当我们通过调用 add 函数更改了 calculator.js 中的 count 值时,index.js 中 count 的值也随之变化。并且 ES6 Module 规定不能对导入的变量进行修改,当我们尝试去修改时它会抛出该变量只读的错误。

循环依赖

循环依赖是指模块 A 依赖于模块 B ,同时模块 B 依赖于模块 A ,或者是 A 依赖于 B ,B 依赖于 C ,C 依赖于 D ,最后绕了一大圈,D 又依赖于 A 。当中间模块太多时我们就很难发现 A 和 B 之间存在隐式的循环依赖了。

因此,如何处理循坏依赖是开发者必须要面对的问题。首先看一下在 CommonJS 中循环依赖的例子:

// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports ='This is foo.js';
// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports ='This is bar.js';
// index.js
require('./foo.js');

而当我们运行上面的代码时,实际输出却是:

value of foo:{}
value of bar:This is bar.js

为什么 foo 的值会是一个空对象呢?让我们从头梳理一下代码的实际执行顺序:

  1. index.js 导入了 foo.js ,此时开始执行 foo.js 中的代码。
  2. foo.js 的第 1 句导入了 bar.js ,这时 foo.js 不会继续向下执行,而是会进入 bar.js 内部。
  3. 在 bar.js 中又对 foo.js 进行了导入,这里产生了循环依赖。需要注意的是,执行权并不会再交回 foo.js,而是直接取其导出值,也就是 module.exports 。但由于 foo.js 未执行完毕,导出值在这时为默认的空对象,因此当 bar.js 执行到打印语句时,我们看到控制台中的 value of foo 就是一个空对象。
  4. bar.js 执行完毕,将执行权交回 foo.js 。
  5. foo.js 从 require 语句继续向下执行,在控制台打印出 value of bar (这个值是正确的),整个流程结束。

接下来我们使用 ES6 Module 的方式重写上面的例子:

// foo.js
import bar from './bar.js'
console.log('value of bar:', bar);
export default 'This is foo.js';
// bar.js
import foo from './foo.js';
console.log('value of foo:', foo);
export default This is bar.js';
// index.js
import foo from './foo.js';

执行结果如下:

value of foo:undefined
foo.js:3 value of bar:This is bar.js

很遗憾,在 bar.js 中同样无法得到 foo.js 正确的导出值,只不过和 CommonJS 默认导出一个空对象不同,这里获取到的是 undefined 。


上面我们谈到,在导入一个模块时,CommonJS 获取到的是值的副本,ES6 Module 则是动态映射,那么我们能否利用 ES6 Module 的特性使其支持循环依赖呢?请看下面这个例子:

// index.js
import foo from './foo.js';
foo ('index.js');
// foo.js
import bar from './bar.js'
function foo(invoker){console.log(invoker + 'invokes foo.js');bar ('foo.js');
}
export default foo;
// bar.js
import foo from './foo.js'
let invoked = false;
function bar (invoker){if(!invoked){invoked = true;console.log(invoker + 'invokes bar.js');foo ('bar.js');}
}
export default bar;

上面代码的执行结果如下:

index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js

可以看到,foo.js 和 bar.js 这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。

  1. index.js 作为入口导入了 foo.js ,此时开始执行 foo.js 中的代码。
  2. 从 foo.js 导入 bar.js ,,执行权交给 bar.js 。
  3. 在 bar.js 中一直执行到结束,完成 bar 函数的定义。注意,此时由于 foo.js 还没执行完,foo 的值现在仍然是undefined
  4. 执行权回到 foo.js 继续执行直到结束,完成 foo 函数的定义。由于 ES6 Module 动态映射的特性,此时在 bar.js 中 foo 的值已经从 undefined 成为我们定义的函数,这是与 CommonJS 在解决循环依赖时的本质区别,CommonJS 中导入的是值的副本,不会随着模块中原有值的变化而变化。
  5. 执行权回到 index.js 并调用 foo 函数,此时会依次执行 foo一bar一foo ,并在控制台输出正确的值。

由上面的例子可以看出,ES6 Module 的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11787.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【leetcode100】路径总和Ⅲ

1、题目描述 给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点…

解锁数据结构密码:层次树与自引用树的设计艺术与API实践

1. 引言:为什么选择层次树和自引用树? 数据结构是编程中的基石之一,尤其是在处理复杂关系和层次化数据时,树形结构常常是最佳选择。层次树(Hierarchical Tree)和自引用树(Self-referencing Tree…

python-leetcode-二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣(LeetCode) # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right from coll…

c++可变参数详解

目录 引言 库的基本功能 va_start 宏: va_arg 宏 va_end 宏 va_copy 宏 使用 处理可变参数代码 C11可变参数模板 基本概念 sizeof... 运算符 包扩展 引言 在C编程中,处理不确定数量的参数是一个常见的需求。为了支持这种需求,C标准库提供了 &…

w191教师工作量管理系统的设计与实现

🙊作者简介:多年一线开发工作经验,原创团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取,记得注明来意哦~🌹赠送计算机毕业设计600个选题excel文…

Vuex状态管理

1、Vuex 是什么? Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 简单理解 Vuex可以帮我们管理全局的属性,并且是是响应式的&…

DBASE DBF数据库文件解析

基于Java实现DBase DBF文件的解析和显示 JDK19编译运行,实现了数据库字段和数据解析显示。 首先解析数据库文件头代码 byte bytes[] Files.readAllBytes(Paths.get(file));BinaryBufferArray bis new BinaryBufferArray(bytes);DBF dbf new DBF();dbf.VersionN…

亚博microros小车-原生ubuntu支持系列:20 ROS Robot APP建图

依赖工程 新建工程laserscan_to_point_publisher src/laserscan_to_point_publisher/laserscan_to_point_publisher/目录下新建文件laserscan_to_point_publish.py #!/usr/bin/env python3import rclpy from rclpy.node import Node from geometry_msgs.msg import PoseStam…

冷启动+强化学习:DeepSeek-R1 的原理详解——无需监督数据的推理能力进化之路

本文基于 DeepSeek 官方论文进行分析,论文地址为:https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf 有不足之处欢迎评论区交流 原文翻译 在阅读和理解一篇复杂的技术论文时,逐字翻译是一个重要的步骤。它不仅能帮助我们准确把握作者的原意,还能为后续…

优选算法的灵动之章:双指针专题(一)

个人主页:手握风云 专栏:算法 一、双指针算法思想 双指针算法主要用于处理数组、链表等线性数据结构中的问题。它通过设置两个指针,在数据结构上进行遍历和操作,从而实现高效解决问题。 二、算法题精讲 2.1. 查找总价格为目标值…

数据结构之栈和队列(超详解)

文章目录 概念与结构栈队列 代码实现栈栈是否为空,取栈顶数据、栈的有效个数 队列入队列出队列队列判空,取队头、队尾数据,队列的有效个数 算法题解有效的括号用队列实现栈用栈实现队列复用 设计循环队列数组结构实现循环队列构造、销毁循环队…

解析 Oracle 中的 ALL_SYNONYMS 和 ALL_VIEWS 视图:查找同义词与视图的基础操作

目录 前言1. ALL_SYNONYMS 视图2. ALL_VIEWS 视图3. 扩展 前言 🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF 1. ALL_SYNONYMS 视图 在 Oracle 数据库中,同义词(Synonym)是对数…

DeepSeek-R1 本地部署教程(超简版)

文章目录 一、DeepSeek相关网站二、DeepSeek-R1硬件要求三、本地部署DeepSeek-R11. 安装Ollama1.1 Windows1.2 Linux1.3 macOS 2. 下载和运行DeepSeek模型3. 列出本地已下载的模型 四、Ollama命令大全五、常见问题解决附:DeepSeek模型资源 一、DeepSeek相关网站 官…

【C语言入门】解锁核心关键字的终极奥秘与实战应用(二)

目录 一、sizeof 1.1. 作用 2.2. 代码示例 二、const 2.1. 作用 2.2. 代码示例 三、signed 和 unsigned 3.1. 作用 3.2. 代码示例 四、struct、union、enum 4.1. struct(结构体) 4.1.1. 作用 4.1.2. 代码示例 4.2. union(联合…

如何确认Linux嵌入式系统的触摸屏对应的是哪个设备文件?如何查看系统中所有的输入设备?输入设备的设备文件有什么特点?

Linux嵌入式系统的输入设备的设备文件有什么特点? 在 Linux 中,所有的输入设备(如键盘、鼠标、触摸屏等)都会被内核识别为 输入事件设备,并在 /dev/input/ 目录下创建相应的 设备文件,通常是: …

ESP32-c3实现获取土壤湿度(ADC模拟量)

1硬件实物图 2引脚定义 3使用说明 4实例代码 // 定义土壤湿度传感器连接的模拟输入引脚 const int soilMoisturePin 2; // 假设连接到GPIO2void setup() {// 初始化串口通信Serial.begin(115200); }void loop() {// 读取土壤湿度传感器的模拟值int sensorValue analogRead…

Hive:窗口函数(1)

窗口函数 窗口函数OVER()用于定义一个窗口,该窗口指定了函数应用的数据范围 对窗口数据进行分区 partition by 必须和over () 一起使用, distribute by经常和sort by 一起使用,可以不和over() 一起使用.DISTRIBUTE BY决定了数据如何分布到不同的Reducer上&#xf…

【react-redux】react-redux中的 useDispatch和useSelector的使用与原理解析

一、useSelector 首先,useSelector的作用是获取redux store中的数据。 下面就是源码,感觉它的定义就是首先是createSelectorHook这个方法先获得到redux的上下文对象。 然后从上下文对象中获取store数据。然后从store中得到选择的数据。 2、useDispatc…

java异常处理——try catch finally

单个异常处理 1.当try里的代码发生了catch里指定类型的异常之后,才会执行catch里的代码,程序正常执行到结尾 2.如果try里的代码发生了非catch指定类型的异常,则会强制停止程序,报错 3.finally修饰的代码一定会执行,除…

传输层协议 UDP 与 TCP

🌈 个人主页:Zfox_ 🔥 系列专栏:Linux 目录 一:🔥 前置复盘🦋 传输层🦋 再谈端口号🦋 端口号范围划分🦋 认识知名端口号 (Well-Know Port Number) 二&#xf…