CommonJS和EsModule导入导出

375 阅读5分钟

一. CommonJS

初级前端开发,菜鸟小白,自己网上总结的,有不对的欢迎大佬指正

CommonJS规范,他的模块编译编译比较有趣,我不知道大家有没有在node环境执行js文件时,直接打印过arguments,如果打印过,你会发现一件非常好玩的事情: image.png 没错,他是可以打印出来的,而且还有好几个参数,我们知道arguments是只有在函数内才能被使用的,他的值是函数的参数列表,那么为什么在node下可以被直接打印呢? 这还得从nodejs的执行特性说起,在node中每个文件都是一个模块,他的执行会被包裹在一个立即执行函数里面

(function (exports, require, module, __filename, __dirname) {
    // 我们的文件代码
});

下面我们就从这个参数列表开始说:

  1. exports 顾名思义,这个就是当前文件的导出内容,指的就是我们写的exports对象,这里又有两个容易混淆的变量,module.exportsexports,实际上exports是module.exports的别名,他们两个最初指向同一块内存地址,但是注意我们这里arguments的第一个参数是exports而不是module.exports。因此当我们使用module.exports导出时,要注意 image.png 在这里由于最初exports和module.exports是同一个引用,但是代码中改变了module.exports的引用,而exports没变指向的依然是最初的空对象 image.png 当我们修改module.exports的属性值,而不是去修改他的引用。他打印的就是正确的了 image.png 当然exports导出一样是可以的。当然有一点,这个arguments在开头打印是没有用的,因为代码还没执行到exports.a,此时打印的arguments的第一项依然是空对象

这时候大家可能想到我们平时module.exports={}不是也用的挺多的吗,那是因为这里的exports参数和我们平时require引入的又不是一个东西了,我们平时require引入的其实是module.exports导出的内容,这里我感觉也挺乱的,但确实是这样。

  1. 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
  1. module

这个在上面说require.main时就提到了,当时有人可能想到这个变量为什么可以直接使用,因为在编译的时候这个变量就已经被直接注入进来了,里面有当前模块的一些信息,比如路径,文件名,导出内容等等,可以自己打印看一下。

  1. __dirname

这个是我们经常使用的变量,实际上他也是在编译的时候被注入进来,表示当前文件所在文件夹的绝对路径, 我们经常在获取某个文件的绝对路径是会使用到他

const path = require("path")

const resolve = path.resolve(__dirname, "./1.js")
  1. __fileName

这个和__dirname差不多,表示的是当前文件的绝对路径

二. EsModule

  1. 编译时输出

这是因为在浏览器环境,他去引入文件是要通过网络请求去加载。和服务器环境不一样,服务器去读取处理文件效率是非常高的,因此他不需要考虑这些速度问题。可以试想一下,一个项目中成千上万个导入,都是在运行时才去加载,再慢慢的去请求文件,那速度不用想 因此,模块的导入和导出在编译阶段就已经确定。

  1. 异步加载
import("./a.js")

这个异步加载很好理解,我们在浏览器环境去导入别的文件,其实都是发起网络请求,那网络请求当然是异步的。 上面的写法也是动态导入的写法,同时动态导入不会影响后续代码的执行。

import {a} from "./a.js"

这个导入在运行前就已经请求好文件,然后在模块加载完毕后,确定好变量的引用。

  1. “符号连接”-值的引用

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

总结

区别,抄一下阮一峰大神的话

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。