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
就不会,他是静态分析全量查找。
commonjs
和 esmodule
最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行时;而静态则表示模块依赖关系的建立发生在代码编译阶段。
esModule
是静态分析,在代码的编译阶段就可以分析出模块的依赖关系。它相比于commonjs
来说具备以下几点优势。
- 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或者接口,但有可能会将其代码完整地加载进来。未被调用到的模块永远不会执行,也就成了死代码。通过静态分析可以在打包时去掉这些没有被使用的代码,减小打包资源体积。
- 模块变量类型检测。
javascript
属于动态类型语言,不会在代码执行前检测类型错误(比如对一个字符串类型的值进行函数调用)。esModule
的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。 - 编译器优化。在
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
。最初时,exports
和module.exports
指向同一个空对象。但如果直接给exports
赋值一个新的对象,那么exports
和module.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.js
,a脚本
先输出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 对象作为未完全解析的标记。这是由于它们在加载和解析模块时的不同机制导致的。
如何避免被修改
- 如果只是一层对象,那么使用
- 如果有对象嵌套,则需要递归将每个对象都需要进行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
做了以下操作:
- 将要导出的默认成员赋值给一个默认导出变量。
- 将默认导出变量标记为默认导出。
- 在编译后的代码中,将默认导出变量与模块对象的
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';
直接访问到默认导出的成员。
需要注意的是,一个模块只能有一个默认导出,而且默认导出在导入时可以在导入语句中使用任意名称来接收,默认导出没有使用 {}
进行解构赋值。