commonjs循环依赖,esm循环依赖,是否可以修改导出值,如何避免被修改

参考

CommonJs

总结:require一个文件,是同步执行该文件,当执行了该文件后,会将文件导出值缓存起来,下次再次访问它时,只需要取出缓存即可,不需要二次执行该文件

CommonJS 规范是一种用于 JavaScript 模块化开发的规范,它定义了如何组织、导入和导出模块。它最初在服务器端的 Node.js 环境中得到广泛应用,后来也被一些构建工具和框架广泛采用。

根据 CommonJS 规范,每个模块都是一个单独的文件,模块内部的代码是私有的,不会污染全局命名空间。模块通过 module.exports 导出自己的公共接口,其他模块可以通过 require() 函数引入这个模块,并使用 require() 返回的对象或函数来访问模块的导出。

以下是一个使用 CommonJS 规范的简单示例:

moduleA.js:

const message = 'Hello, World!';

function sayHello() {
  console.log(message);
}

module.exports = {
  sayHello
};

moduleB.js:

const moduleA = require('./moduleA');

moduleA.sayHello(); // 输出:Hello, World!

在上面的示例中,moduleA.js 导出了 sayHello 函数,moduleB.js 使用 require() 函数引入了 moduleA 模块,并调用了 sayHello() 函数。

CommonJS 规范的主要特点是同步加载,即模块的依赖关系在运行时会被解析,并且模块的代码会被立即执行。这种方式适用于服务器端,因为文件系统的加载通常是同步的。然而,在浏览器端,同步加载会阻塞页面的加载,因此新的规范如 ES Modules (ESM) 被引入,并逐渐取代了 CommonJS 规范在浏览器环境下的应用。

CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作。

CommonJS规范中的module、exports和require

  • 每个文件就是一个模块,有自己的作用域。每个模块内部,module变量代表当前模块,是一个对象,它的exports属性(即module.exports)是对外的接口。
  • module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
  • 为了方便,Node为每个模块提供一个exports变量,指向module.exports。
let exports = module.exports;
  • require命令用于加载模块文件。

使用示例:

  //name.js
  exports.name = function(){return '李婷婷'}; //导出
  //getName.js
  let getName = require('name'); //引入

注:不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系:如下

exports = function(x){console.log(x)}

如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。

CommonJS模块导入用require,导出用module.exports。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以使用时要注意,esmodule这种情况是不允许改的,会报错,但是哈,两种模式,导出的引用类型的属性,是可以更改的,如果不让改,可以考虑用冻结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Esm

esm 导出的是 内存地址,别的文件引入时,会指向同一个地址

在这里插入图片描述
在这里插入图片描述

commonjs和esModule的区别

commonjs在代码运行时,凡是遇到require字段,那么就立马去执行该文件,在该文件内,如果require了原来的文件,形成了循环依赖了,它不会报错,而是以原来文件内require之前导出的对象为暂时内容,给该文件去使用,如果没有导出,默认就是空对象了,注意点是,js文件内容全部执行完毕后,后续遇到再次导入该文件了,他会把上次执行完毕后导出的值作为内容,给它使用,不存在后续重复执行该文件。
esmodule,遇到了import,只是关联一下目标文件而已,他不是立马就去执行目标文件,如果当前文件使用了目标文件的变量时,是去目标文件内,全量查找,如果在目标文件内,又import当前文件,在目标文件内的方法又引用了当前文件的属性,此时就形成了循环依赖,如果是commonjs他会报错,因为commonjs是使用require之前导出的内容,而不是全部文件的导出,于是报错,目标属性不存在,esmodule就不会,他是静态分析全量查找。

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

esModule是静态分析,在代码的编译阶段就可以分析出模块的依赖关系。它相比于commonjs来说具备以下几点优势。

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

在导入一个模块时,对于commonjs来说获取的是一份导出值的副本;而在esModule中则是值的动态映射,并且这个映射是只读的。

// CommonJs 的分析
// a.js
var count = 0;
var obj = { name: "zan" };
module.exports = {
  count: count,
  obj,
  logObj: function () {
    console.log(obj.name);
  },
  add: function (a, b) {
    count += 1;
    return a + b;
  },
};

// b.js
var count = require("./a.js").count;
var obj = require("./a.js").obj;
var logObj = require("./a.js").logObj;
var add = require("./a.js").add;
console.log(count); // 0 副本值
add(2, 3); // 更改原本,但不影响副本值
console.log(count); // 0
count += 1; // 副本值可以更改,但是原本值没有被改
console.log(count); // 1

obj.name = "lan";
console.log(obj.name); // "lan"
logObj() // "lan"
// EsModule 的分析
// a.js
var count = 0;
var obj = { name: "zan" };
function logObj() {
  console.log(obj.name);
}
function add(a, b) {
  count += 1;
  return a + b;
}
export { count, obj, logObj, add };

// b.js
import { count, obj, logObj, add } from "./a.js";
console.log(count); // 0 副本值
add(2, 3); // 更改原本,但不影响副本值
console.log(count); // 1
// count += 1; // Uncaught TypeError: Assignment to constant variable.
obj.name = "lan";
console.log(obj.name); // "lan"
logObj(); // "lan"

exports和module.exports 区别

在 Node.js 中,“exports”和“module.exports”两者的区别有以下几点:

导出对象类型不同: exports 是对 module.exports 的一个全局引用,而实际导出的是 module.exports 对象。即 exports 只是为了方便,可以在不断开 module.exports 的情况下,挂载属性和方法到 exports 上,最终再将其赋值给 module.exports,从而达到返回多个 API 的目的。

直接赋值的效果不同:当直接对 exports 赋值时,它不会覆盖 module.exports 的引用,因此不能使用 exports 代替 module.exports 赋值(i.e., 模块导出的内容)。

exports对象是module.exports的一个别名,即 exports = module.exports。最初时,exportsmodule.exports指向同一个空对象。但如果直接给exports赋值一个新的对象,那么exportsmodule.exports将不再指向同一个对象,而module.exports仍然作为默认的导出对象。

下面是一个示例来说明这个区别:

// greeting.js

// 方式1:
module.exports.sayHello = function(name) {
  console.log(`Hello, ${name}!`);
};

// 方式2:
exports.sayGoodbye = function(name) {
  console.log(`Goodbye, ${name}!`);
};

// 方式3:
exports = {
  sayHi: function(name) {
    console.log(`Hi, ${name}!`);
  }
};

在上述示例中,方式1和方式2都是正确的导出方法。sayHello和sayGoodbye方法都可以从外部访问。但是,方式3中的sayHi方法是不会被导出的,因为它将exports对象重新赋值为一个新对象,exports和module.exports不再相等,而默认的导出对象仍然是module.exports,没有被修改。

所以,如果要导出一个模块,最安全和推荐的方式是使用module.exports,而避免直接赋值给exports对象。但在大多数情况下,使用exports也是有效的。

循环依赖

循环加载指的是a脚本的执行依赖b脚本,b脚本的执行依赖a脚本

  • CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
  • ES6模块对导出模块,变量,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。

CommonJS模块规范主要适用于后端Node.js,后端Node.js是同步模块加载,所以在模块循环引入时模块已经执行完毕。推荐前端工程中使用ES6的模块规范,通过安装Babel转码插件支持ES6模块引入的语法。

CommonJS模块的加载原理

CommonJS模块就是一个脚本文件,require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成该模块的一个说明对象。

{
    id: '',  //模块名,唯一
    exports: {  //模块输出的各个接口
        ...
    },
    loaded: true,  //模块的脚本是否执行完毕
    ...
}

以后用到这个模块时,就会到对象的exports属性中取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存中取值。

CommonJS模块是加载时执行,即脚本代码在require时就全部执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。

案例说明:

//a.js
exports.done = false;

var b = require('./b.js');
console.log('在a.js中,b.done = %j', b.done);

exports.done = true;
console.log('a.js执行完毕!')

//b.js
exports.done = false;

var a = require('./a.js');
console.log('在b.js中,a.done = %j', a.done);

exports.done = true;
console.log('b.js执行完毕!')

//main.js
var a = require('./a.js');
var b = require('./b.js');

console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);

输出结果如下:

//node环境下运行main.js
node main.js

在b.js中,a.done = false
b.js执行完毕!
在a.js中,b.done = true
a.js执行完毕!
在main.js中,a.done = true, b.done = true

JS代码执行顺序如下:

1)main.js中先加载a.jsa脚本先输出done变量,值为false,然后加载b脚本a的代码停止执行,等待b脚本执行完成后,才会继续往下执行。

2)b.js执行到第二行会去加载a.js,这时发生循环加载,系统会去a.js模块对应对象的exports属性取值,因为a.js没执行完,从exports属性只能取回已经执行的部分,未执行的部分不返回,所以取回的值并不是最后的值。

3)a.js已执行的代码只有一行,exports.done = false;所以对于b.js来说,require a.js只输出了一个变量done,值为false。往下执行console.log('在b.js中,a.done = %j', a.done);控制台打印出:

在b.js中,a.done = false

4)b.js继续往下执行,done变量设置为true,console.log(‘b.js执行完毕!’),等到全部执行完毕,将执行权交还给a.js。此时控制台输出:

b.js执行完毕!

5)执行权交给a.js后,a.js接着往下执行,执行console.log(‘在a.js中,b.done = %j’, b.done);控制台打印出:

在a.js中,b.done = true

6)a.js继续执行,变量done设置为true,直到a.js执行完毕。

a.js执行完毕!

7)main.js中第二行不会再次执行b.js,直接输出缓存结果。最后控制台输出:

在main.js中,a.done = ``true``, b.done = ``true

总结:

1)在b.js中,a.js没有执行完毕,只执行了第一行,所以循环加载中,只输出已执行的部分。

2)main.js第二行不会再次执行,而是输出缓存b.js的执行结果。

exports.done = true;

ES6模块的循环加载

ES6模块与CommonJS有本质区别,ES6模块对导出变量,方法,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。

案例说明:

//even.js
import {odd} from './odd';

var counter = 0;
export function even(n){
    counter ++;
    console.log(counter);

    return n == 0 || odd(n-1);
}

//odd.js
import {even} from './even.js';

export function odd(n){
    return n != 0 && even(n-1);
}

//index.js
import * as m from './even.js';

var x = m.even(5);
console.log(x);

var y = m.even(4);
console.log(y);

执行index.js,输出结果如下:

babel-node index.js

1
2
3
false
4
5
6
true

可以看出counter的值是累加的,ES6是动态引用。如果上面的引用改为CommonJS代码,会报错,因为在odd.js里,even.js代码并没有执行。改成CommonJS规范加载的代码为:

//even.js
var odd = require('./odd.js');

var counter = 0;
module.exports = function even(n){
    counter ++;
    console.log(counter);

    return n == 0 || odd(n-1);
}
//odd.js
var even = require('./even.js');

module.exports = function odd(n){
    return n != 0 && even(n-1);
}
//index.js
var even = require('./even.js');

var x = even(5);
console.log(x);

var y = even(5);
console.log(y);

执行index.js,输出结果如下:

$ babel-node index.js
1
/Users/name/Projects/node/ES6/odd.1.js:6
    return n != 0 && even(n - 1);
                     ^

TypeError: even is not a function
    at odd (/Users/name/Projects/node/ES6/odd.1.js:4:22)

可以修改导出值

在这里插入图片描述
在这里插入图片描述

commonjs循环依赖,esm循环依赖,他们是如何处理的,处理方式有什么区别

在 CommonJS 和 ES Modules (ESM) 中,循环依赖的处理方式有些不同。

在 CommonJS 中,当发生循环依赖时,CommonJS 会返回一个未完全加载的模块导出对象(module.exports)的引用,这是因为 CommonJS 的模块加载是同步的,而它在运行时加载模块。这样可以保证循环依赖时,每个模块都能够获取到正确的导出对象,但是这个导出对象可能不是完全加载的。

例如,有两个模块 A 和 B 分别依赖于对方:
A.js:

const B = require('./B');
module.exports.A = 'A';
console.log(B.B);

B.js:

const A = require('./A');
module.exports.B = 'B';
console.log(A.A);

在这种情况下,当 A.js 加载时,它会发现它需要依赖于 B.js,然后它会将 B.js 的内容执行,并返回一个未完全加载的导出对象,这时 console.log(B.B) 会输出 undefined。然后,当 B.js 加载时,它发现它需要依赖于 A.js,同样地,会返回一个未完全加载的导出对象,这时 console.log(A.A) 会输出 ‘A’。

相比之下,ESM 的循环依赖处理方式稍微不同。ESM 采用静态分析的方式,在代码解析阶段就确定模块之间的依赖关系,它是异步加载模块的,所以在解析阶段就能够发现循环依赖。

当 ES Modules 遇到循环依赖时,它不会返回一个未完全加载的导出对象,而是将该模块的导出对象设置为一个尚未被完全解析的状态(即一个 Promise 对象)。这是为了避免导致出现未完全加载的对象。

例如,使用 ESM 的情况下,模块 A 和 B 互相依赖:
A.js:

import { B } from './B.js';
export const A = 'A';
console.log(B);

B.js:

import { A } from './A.js';
export const B = 'B';
console.log(A);

当 A.js 加载时,它会发现它需要依赖于 B.js,然后它会创建一个未完全解析的模块对象,此时 console.log(B) 输出的是一个 Promise 对象。然后,当 B.js 加载时,由于它也需要依赖于 A.js,ESM 可以检测到循环依赖,因此会返回一个尚未完全解析的模块对象,此时 console.log(A) 也会输出一个 Promise 对象。

总结来说,CommonJS 和 ESM 在处理循环依赖时的方式有一些不同。CommonJS 会返回一个未完全加载的导出对象的引用,而 ESM 则使用 Promise 对象作为未完全解析的标记。这是由于它们在加载和解析模块时的不同机制导致的。

如何避免被修改

  1. 如果只是一层对象,那么使用在这里插入图片描述
  2. 如果有对象嵌套,则需要递归将每个对象都需要进行Object.freeze

export default 和 export的区别

不可以这么写

const myDefault = 'default value';
export myDefault;

示例 1:

const myDefault = 'default value';
export { myDefault }; // 导出 myDefault

示例 2:

const myDefault = 'default value';
export default myDefault; // 导出默认成员 myDefault

这两种导出方式是分别对应着不同的导入方式:

示例 1 的导入方式:

import { myDefault } from './moduleA';
console.log(myDefault); // 输出 'default value'

示例 2 的导入方式:

import myDefault from './moduleA';
console.log(myDefault); // 输出 'default value'

需要注意的是,对于默认导出的成员,无需使用 {} 进行解构赋值,而是直接通过 import 导入。而对于非默认导出的成员,在导入时需要使用 {} 进行解构赋值。

export default 相当于 export做了哪些操作

export default 是 ES6 模块语法中用于导出默认成员的关键字。它相当于把要导出的默认成员赋值给一个特殊的默认导出变量,默认导出变量的名称是任意的,可以根据需要进行命名。

具体来说,export default 做了以下操作:

  1. 将要导出的默认成员赋值给一个默认导出变量。
  2. 将默认导出变量标记为默认导出。
  3. 在编译后的代码中,将默认导出变量与模块对象的 default 属性关联起来。

举个例子:

// moduleA.js
const myDefault = 'default value';
export default myDefault;

编译后的代码将类似于:

// moduleA.js
const myDefault = 'default value';
export default myDefault;
// 编译后的代码
module.exports.__esModule = true;
module.exports.default = myDefault;

在上述示例中,export default myDefault;myDefault 变量的值赋值给默认导出变量,并将默认导出变量与模块对象的 default 属性关联起来。这样其他模块在导入时,可以使用 import myDefault from './moduleA'; 直接访问到默认导出的成员。

需要注意的是,一个模块只能有一个默认导出,而且默认导出在导入时可以在导入语句中使用任意名称来接收,默认导出没有使用 {} 进行解构赋值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值