一. CommonJS
初级前端开发,菜鸟小白,自己网上总结的,有不对的欢迎大佬指正
CommonJS规范,他的模块编译编译比较有趣,我不知道大家有没有在node环境执行js文件时,直接打印过arguments,如果打印过,你会发现一件非常好玩的事情:
没错,他是可以打印出来的,而且还有好几个参数,我们知道arguments是只有在函数内才能被使用的,他的值是函数的参数列表,那么为什么在node下可以被直接打印呢?
这还得从nodejs的执行特性说起,在node中每个文件都是一个模块,他的执行会被包裹在一个立即执行函数里面
(function (exports, require, module, __filename, __dirname) {
// 我们的文件代码
});
下面我们就从这个参数列表开始说:
exports顾名思义,这个就是当前文件的导出内容,指的就是我们写的exports对象,这里又有两个容易混淆的变量,module.exports和exports,实际上exports是module.exports的别名,他们两个最初指向同一块内存地址,但是注意我们这里arguments的第一个参数是exports而不是module.exports。因此当我们使用module.exports导出时,要注意在这里由于最初exports和module.exports是同一个引用,但是代码中改变了module.exports的引用,而exports没变指向的依然是最初的空对象
当我们修改module.exports的属性值,而不是去修改他的引用。他打印的就是正确的了
当然exports导出一样是可以的。当然有一点,这个arguments在开头打印是没有用的,因为代码还没执行到exports.a,此时打印的arguments的第一项依然是空对象
这时候大家可能想到我们平时module.exports={}不是也用的挺多的吗,那是因为这里的exports参数和我们平时require引入的又不是一个东西了,我们平时require引入的其实是module.exports导出的内容,这里我感觉也挺乱的,但确实是这样。
require
从打印结果可以看到里面有4项内容
require.resolve
这个是解析文件的绝对路径
const path = require.resolve('./example');
console.log(path);
// 输出: /绝对路径/example.js
require.main
require.main 是一个对象,它指向 Node.js 进程启动时的入口模块,也就是 require.main 引用的模块是用户通过命令行启动的那个模块。 常用于判断当前模块是否是主模块(即是否是通过命令行直接运行的模块),而不是作为被 require 的模块。
//运行node ./main.js
// main.js
require("./help")
console.log('require.main === module in main.js:', require.main === module); //true
//help.js
console.log('require.main === module in help.js:', require.main === module) //false
require.extensions
require.extensions 是一个对象,用于定义 Node.js 如何处理不同类型的文件扩展名(如 .js、.json、.node 等),但是现在已经废弃了,了解即可
require.cache
这是非常重要的一个属性,他也可以解释为什么说require引入的是值的拷贝,在commonJS规范下,模块的导出是在编译时就进行的。在编译时,遇到文件中有module.export或者exports的时候,他会将这个导出内容缓存起来,而缓存的地方正是这个位置require.cache。这样后续其他文件再去引入这个文件的时候,就直接取缓存的结果,而不需要重新去引入执行文件了。
而由于他的导出是被缓存起来了,当我们导出一个基本类型数据,后面再去修改他的时候,在引入文件里的这个数据是不会变的。
// help.js
let a = 5;
module.exports = {
a,
fn: () => {
a++;
}
}
// a.js
const { a, fn } = require("./help")
console.log("a=", a); //5
fn();
console.log("变化后a=", a); //5
module
这个在上面说require.main时就提到了,当时有人可能想到这个变量为什么可以直接使用,因为在编译的时候这个变量就已经被直接注入进来了,里面有当前模块的一些信息,比如路径,文件名,导出内容等等,可以自己打印看一下。
__dirname
这个是我们经常使用的变量,实际上他也是在编译的时候被注入进来,表示当前文件所在文件夹的绝对路径, 我们经常在获取某个文件的绝对路径是会使用到他
const path = require("path")
const resolve = path.resolve(__dirname, "./1.js")
__fileName
这个和__dirname差不多,表示的是当前文件的绝对路径
二. EsModule
- 编译时输出
这是因为在浏览器环境,他去引入文件是要通过网络请求去加载。和服务器环境不一样,服务器去读取处理文件效率是非常高的,因此他不需要考虑这些速度问题。可以试想一下,一个项目中成千上万个导入,都是在运行时才去加载,再慢慢的去请求文件,那速度不用想 因此,模块的导入和导出在编译阶段就已经确定。
- 异步加载
import("./a.js")
这个异步加载很好理解,我们在浏览器环境去导入别的文件,其实都是发起网络请求,那网络请求当然是异步的。 上面的写法也是动态导入的写法,同时动态导入不会影响后续代码的执行。
import {a} from "./a.js"
这个导入在运行前就已经请求好文件,然后在模块加载完毕后,确定好变量的引用。
- “符号连接”-值的引用
JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
总结
区别,抄一下阮一峰大神的话
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。