个人博客

FOGBeta

CommonJS与ES6Module的区别

1
2024-06-04

CommonJS

CommonJS 是 2009 年提出的包含模块、文件、IO、控制台在内的一系列标准。Node.js 的实现中采用了 CommonJS 标准的一部分,而非它的原始定义,现在一般谈到 CommonJS 其实是 Node.js 中的版本。

CommonJS 最初只为服务端而设计(因为在服务端需要与操作系统和其他应用程序互动,否则无法编程),直到有了 Browserify(一个运行在Node.js环境下的模块打包工具,可以将 CommonJS 模块打包为浏览器可以运行的单个文件),这也就意味着客户端的代码也可以遵循 CommonJS 标准来编写了。而且 Node.js 的包管理器 npm 允许开发者获取他人的代码库,以及发布自己的代码库,这种共享的传播方式使 CommonJS 在前端开发更加流行起来。

模块

CommonJS 中规定每个文件就是一个模块,会形成一个属于模块自身的作用域,所有变量只有自己能访问,外部不可见。

导出

CommonJS 中通过 module.exports (简化的为 exports)导出模块中内容,导出是模块向外暴露自身的唯一方式。

注意:浏览器是无法识别 CommonJS 模块的,所有以下这些 demo 需要在 Node.js 环境中去测试。

module.exports = {
  name: 'calculator',
  add: (a, b) => a + b
}

可以理解为,CommonJS 模块内部会用一个 module 对象存放当前模块的信息,其中 module.exports 用来指定该模块要对外暴露的内容,简化的导出方式可以直接使用 exports

exports.name = 'calculator'
exports.add = (a, b) => a + b

这两段代码实现效果上没有任何不同,其内在机制是将 exports 指向 module.exports

每个模块的最开始定义可以理解为:

let module = {
  // 模块内容
  // ...
  exports: {} // 模块要暴露内容的地方
}
​
let exports = module.exports

因此,在使用 exports 时要注意不要直接给它赋值,否则会切断它和 module.exports 的关系而使其失效。 通过模块定义就可以判断,当一个模块中既有 exports 又有 module.exports 导出内容时,最终到底导出的内容是什么,比如:

exports.add = (a, b) => a + b
​
module.exports = {
  name: 'calculator' // 只有这里的内容被导出了
}

另外,模块导出语句末尾的代码还是会照常执行的,只是,在实际使用中,为了提高可读性,不建议在导出语句后还写其他内容。

导入

CommonJS 中使用 require 语句进行模块导入,module.exports 对象作为其返回值返回。

// calculator.js
module.exports = {
  add: (a, b) => a + b
}
​
// index.js
const calculator = require('./calculator')
console.log(calculator.add(1, 2))

执行:

node index.js

结果:

// 3

当使用 require 导入一个模块时有两种情况

  • 该模块未曾被加载过,这时会首先执行该模块,然后获取到该模块最终导出的内容

  • 该模块已经被加载过,这时该模块的代码不会再执行,而是直接获取该模块上一次导出的内容

请看下面的例子说明:

// calculator.js
console.log('我被执行啦~~~')
module.exports = {
  name: 'calculator',
  add: (a, b) => a + b
}
​
// index.js
const name = require('./calculator').name
console.log(name)
const add = require('./calculator').add
console.log(add(1, 2))

执行:

node index.js

结果:

// 我被执行啦~~~
// calculator
// 3

这是因为,前面我们说模块有一个 module 对象用来存放其信息,其中有一个属性 loaded 用于记录该模块是否被加载过,第一次被加载时值被赋为 true,后面再次加载时检查这个值为 true 就不会再执行模块代码了。

有时候加载一个模块时,不需要获取其导出的内容,只需要执行这个模块代码,就直接导出 require 即可,并且 require 还可以接受表达式,例如:

const moduleNames = ['foo.js', 'bar.js']
moduleNames.forEach(name => {
  require('./' + name)
})

ES6 Module

JavaScript 设计之初并没有包含模块的概念,基于越来越多的工程需要,为了使用模块化开发,JavaScript 社区涌现了多种模块标准,包括上述所说的 CommonJS。直到2015年,发布了 ES6(ECMAScript 6.0),自此 JavaScript 语言才具备了模块这一特性(JavaScript 模块)。

模块

ES6 Module 也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。ES6 的导入导出语句是 importexport

ES6 Module 会自动采用严格模式,即,在 ES6 Module 中不管开头是否有 use strict 都会采用严格模式。

导出

ES6 Module 中使用 export 命令来导出模块,有两种方式

  • 命名导出

  • 默认导出

命名导出

一个模块可以有多个命名导出,有两种不同写法:

// 写法1
export const name = 'calculator'
export const add = (a, b) => a + b

// 写法2
const name = 'calculator'
const add = (a, b) => a + b
export {name, add}

第1种写法是在声明变量的同时用 export 导出;第2种写法是先声明,再用同一个 export 语句导出,两种写法效果一样。

导出时,可以通过 as 关键字对变量重命名:

export {name, add as getSum} // 导入时即为 name 和 getSum

默认导出

默认导出只能有一个

// 导出对象
export default {
  name: 'calculator',
  add: (a, b) => a + b
}
// 导出字符串
export default 'This is a string'
// 导出匿名函数
export default function() {...}

可以将 export default 理解为对外输出了一个名为 default 的变量,因此不需要像命名导出那样进行变量声明,直接导出即可。

导入

ES6 Module 中使用 import 语法导入模块。

导入命名导出的模块

加载带有命名导出的模块时,导入变量的效果相当于在当前作用域下声明了这些变量,并且这些变量只读,不可对其进行更改,也可以通过 as 关键字对导入的变量重命名:

// calculator.js
const name = 'calculator'
const add = (a, b) => a + b
export {name, add}

// index.js
import {name as myName, add} from './calculator.js'
console.log(add(1, 2), myName)

在导入多个变量时,还可以采用整体导入的方式:

// index.js
import * as calculator from './calculator.js'
console.log(calculator.add(1, 2), calculator.name)

因为 ES6 Module 是可以直接在浏览器中运行的模块方式,因此可以通过 HTML 文件直接引入这些脚本文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ES6 Module</title>
</head>
<body>
  <script type="module" src="./index.js"></script>
</body>
</html>