深入对比esModule和commonjs模块化的区别

4,352 阅读6分钟

bg-219.7a1acf4f.svg

前言

commonjs

2009年,Ryan Dahl基于开源的V8引擎发布了nodejs,让开发者可以用js来实现后端的服务,既然要使用js,那就得解决js中存在的两个疑难问题: 全局变量污染依赖混乱, 因为模块化是后端程序开发过程中必不可少的一部分。

经过大佬以及社区的讨论与发展,最终形成了一个解决方案,即大名鼎鼎的commonjs,该方案解决了全局变量污染和模块依赖混乱的问题,随即就被nodejs支持并引入,如是nodejs成为了第一个为JS语言实现模块化的平台,为前端接下来的迅猛发展奠定了实践基础。

esModule

既然node实现了JS模块化方案,那么浏览器中是否也可以呢,如是大佬们尝试把服务器端的commonjs移植到浏览器上,但是结果不理想,毕竟环境差别太大。

那么此路不同,还不如另辟蹊径,于是在社区的努力下,AMD模块化方案诞生,它解决的问题和commonJS一样,只不过可以更好的适应浏览器环境,相继的,CMD规范出炉,它对AMD规范进行了改进,有兴趣的可以去了解一下AMD和CMD。

AMD和CMD的兴起和发展壮大,引起了ECMA官方的关注... 于是在es6发布的版本中,浏览器可以原生支持JS模块, 不用再依赖第三方模块,真正形成了一套统一的标准。

两者区别

1. 引入方式

commonjs

commonjs是动态导入,可以在代码的任何地方引入

const tag = true
if (tag) {
  const mData = require('a.js')
  console.log(mData)
}

esModule

esModule模块是静态导入(在编译阶段进行导入),不能动态加载语句,所以import不能写在块级作用域和判断条件内

ESM的import命令具有提升效果,会提升到整个模块的头部


```js
console.log(mData) // 这里依然可以输出值
import mData from 'a.js'
// 以上代码不会报错,因为import命令是编译阶段执行的,在代码运行之前
const tag = true
if (tag) {
  // 此处引入会报错
  // import mData from 'a.js'
  console.log(mData)
}

2. 使用语法

在代码层面,两者使用的语法不同

commonjs

模块a.js

const obj = {
    name: 'hello world'
}
module.exports = obj

b.js引入a.js

const obj = require('./a.js')
console.log(obj.name)

使用module.exports导出的是一个模块对象,require()引入的是一个模块对象

esModule

模块a.js

export const title = 'es module'
export const message = 'hello world'
const obj = {
    name: 'hello world'
}
export default obj 

b.js引入a.js

import obj, { title, message } from 'a.js'
console.log(title, message, obj.name)

使用export导出指定的数据,import引入具体数据

3. 模块导出导入

commonjs

commonjs模块输出的是一个值的拷贝,很多文章都是这样说的,其实这是不严谨的,很误导人

commonjs导出的是一个module.exports, 导入其实就是给变量赋值(而值可以是原始类型,也可以是引用类型)

  • 当module.exports的值是数字,字符串等原始类型时,赋值是值的拷贝,这样才会产生导出值的改变不会影响到导入值改变的现象
let title = 'hello hi'
setTimeout(() => {
    title = 'hello hehe'
}, 2000)
module.exports = title
const title = require('./a.js')
setTimeout(() => {
   console.log(title)
}, 4000)
console.log(title)

# 输出结果
hello hi
hello hi

可以看出模块内部title的改变,并没有影响到导入模块的值
  • 如果module.exports导出的是一个对象,导出值的改变是否会影响到导入的值,这个跟导入时赋值的方式是有直接关系的
const obj = {
    title: 'hello hi'
}
setTimeout(() => {
    obj.title = 'hello hehe'
}, 2000)
module.exports = obj

直接导入整个对象

const obj = require('./a.js')
setTimeout(() => {
   console.log(obj.title)
}, 4000)
console.log(obj.title)

# 输出结果
hello hi
hello hehe

可以看出输出的值变了,说明模块内部值的变化导致了导入对象值的变化
这里的赋值实际上就是引用赋值,module.exports导出的对象被赋值到导入的模块,两者指向同一块内存空间

导入对象进行解构

const { title } = require('./a.js')
setTimeout(() => {
   console.log(title)
}, 4000)
console.log(title)

# 输出结果
hello hi
hello hi

可以看出输出的值没有发生变化,说明模块内部值的变化没有导致导入值的变化
这里的赋值实际上就是值的拷贝,通过对象解构的方式直接给变量title赋值,所以模块内部对象值的变化不会影响到导入变量值的变化

esModule

esModule模块输出的是一个值的引用, 使用的是动态绑定,esModule导入导出的值都指向同一个内存地址,所以导入值会跟着导出值发生变化

JS引擎对脚本进行静态分析的时候,如果遇到模块加载命令import,就会生成一个只读引用,等到脚本真正执行的时候,再通过这个只读引用,到被加载的模块中去取值。

esModule导入的基本类型值在当前模块不能直接进行修改(代码会报错,但是可以通过调用模块内的方法进行修改),也就是说基本类型的值在被导入的模块中是只读状态,对于导入引用类型的值,可以直接进行修改设置,会对模块内的值产生影响

# 导出模块 a.js
export let title = 'hello hi'
const obj = {
    name: 'tom'
}
export default obj
setTimeout(() => {
    title = 'hello world'
    obj.name = 'tony'
}, 2000)

# 导入模块 b.js
import obj, { title } from './a.js'
console.log(title)
console.log(obj.name)
setTimeout(() => {
    console.log(title)
    console.log(obj.name)
}, 4000)

# 输入结果
hello hi
tom
hello world
tony

可以看出输出的值发生了变化,这是因为导入和导出的值都指向同一内存地址,值变化,取值也就发生变化

cjs输出的是一个对象,该对象需要在模块脚本运行完成后才能生成,而esm输出的是静态的,在编译时就能生成

4. 加载运行

commonjs

commonjs是在代码运行时加载,因为commonjs是导出的整个对象,需要在脚本运行完成后才能生成

esModule

esModule输出的不是一个对象,它是在静态分析编译时输出接口,它的对外接口只是一种静态的定义,可以理解为生成一个个引用变量,当在模块读取的时候再去取

5. 加载模式

commonjs

commonjs使用的是同步加载模块方式,在服务器端大都是读取本地的资源模块,速度非常快,所以采取同步加载方式

esModule

esModule使用的是异步加载模块方式,在浏览器端会请求加载网络资源模块,使用同步加载方式会出现卡顿情况

共同使用

nodejs在v13.2.0版本之后开始支持esmodule模块化,建议开发新的项目使用esmodule模块规范

commonjs项目中使用esModule

在commonjs项目中,想在commonjs规范的模块中引入esmodule模块时是无法实现的, 会报错...

esModule项目中使用commonjs

在ES规范的项目中,想引入commonjs规范的模块是可以实现的,只需要修改文件的后缀为cjs

a.cjs

const obj = {
    title: 'hello world'
}
module.exports = obj

b.js

import obj form './a.cjs'
console.log(obj.title)