一、JS模块化的起源
在早期,JS 是没有 模块化 这一概念的,都是通过 script 标签引入 js 文件代码。当然,这对于写一些简单的需求没有什么问题,但当我们的项目越来越庞大时,我们引入的 js 文件就会越多,这时就会出现以下问题:
- js文件作用域都是顶层,这会造成变量污染
- js文件多,变得不好维护
- js文件依赖问题,稍微不注意顺序引入错,代码全报错
为了解决以上问题 JavaScript 社区出现了 CommonJS,CommonJS 是一种模块化的规范,包括现在的NodeJS 里面也采用了部分 CommonJS 语法在里面。那么在后来 ES6 也正式加入了 ES Module 模块。
二、CommonJS
CommonJS 是一种模块化的规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了体现它的广泛性,修改为 CommonJS,平时我们也会简称为 CJS。
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发。
- Node 是CommonJS在服务器端一个具有代表性的实现;
- Browserify 是CommonJS在浏览器中的一种实现;
- webpack 打包工具具备对CommonJS的支持和转换。
CommonJS规范中的核心变量:exports、module.exports 和 require。
exports 和 module.exports 负责对模块中的内容进行导出;
require 函数负责帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
2.1 exports导出
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会被导出。
不能使用:exports = xxx,在导入后只能拿到一个 {}。
因为 exports 在默认情况下是指向 module.exports 对象的引用,如果为 exports 赋值了,那么也就是说 exports 不再指向 module.exports 所指的对象的地址,而我们向外共享成员的最终结果是 module.exports 所指的对象,如此便会导致错误。
牢记一条原则:无论是使用 exports 还是 module.exports,最终导出的结果都是以 module.exports 所指向的对象为准。
/** bar.js */
exports.name = "张三"
exports.age = 18
exports.sayHello = function() {
console.log("Hello!")
}
/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '张三', age: 18, sayHello: [Function: sayHello] }
2.2 module.exports导出
module.exports 和 exports 除了在用法以外,没有区别!
exports 和 module.exports 指向的是同一个对象(为了不混淆,可以理解为 exports 是 module.exports 对象地址的一个引用,exports 本质是一个变量)
/** bar.js */
// 导出一个对象
module.exports = {
name: '张三',
age: 18,
sex: '男',
height: 1.88
}
// 导出任意值
module.exports.name = '张三'
module.exports.age = undefined
module.exports.sex = null
/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '张三', age: undefined, sex: null, height: 1.88 }
2.3 混合导出
CommonJS 支持混合导出,exports 和 module.exports 可以同时使用,但需要注意覆盖问题的发生。
/** bar.js */
exports.name = "张三"
exports.age = 18
module.exports = {
name: '李四',
age: 24,
}
/** main.js */
const bar = require('./bar')
console.log(bar) // { name: '李四', age: 24 }
2.4 重复导入
模块在被第一次引入时,模块中的js代码会被运行一次
模块被多次引入时,会缓存,最终只加载(运行)一次
/** bar.js */
exports.name = '张三'
console.log('bar.js 运行了~')
/** main.js */
require('./bar') // bar.js 运行了~
require('./bar') // 没有输出 因为没有运行
2.5 动态导入
CommonJS 是运行时加载,所以其支持动态导入,也就是支持在js代码中使用 require 语法。
/** bar.js */
exports.name = 'foo'
/** foo.js */
exports.name = 'foo'
/** main.js */
const list = ['./bar.js', './foo.js']
list.forEach(url => require(url)) // 动态导入
require(list[0]) // 动态导入
2.6 导入值的变化
CommonJs 导出的是值的拷贝,内部对值进行修改不会同步到外部。也可以对拷贝值进行修改。
/** bar.js */
var num = 0
var add = () => ++num
module.exports = { num, add }
/** main.js */
var { num, add } = require('./bar')
console.log(num) // 0
add()
console.log(num) // 0
num = 10
console.log(num) // 10
2.7 CommonJS 总结
- CommonJS 会对加载结果进行缓存;
- CommonJS 是运行时加载模块;
- CommonJS 导出的是值的拷贝,并可以对值进行修改;
- CommonJS 的
require()是同步加载模块。
三、ESModule
从ES6开始,js原生支持模块化,也就是推出了 ESModule,平时我们也会简称为 ESM。
模块功能主要由两个命令构成:export 和 import。 export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
3.1 export导出
export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的 import 命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
// 导出变量
export const name = 'bar'
export const p = {
name: '张三',
age: 14,
}
// 导出函数
export function fn() {}
export const test = () => {}
// 批量多个导出
const cat = 'Tom'
const mouse = 'Jerry'
export {
cat,
mouse,
cat as tom, // 使用as关键字重命名
mouse as jerry,
}
// 默认导出,默认导出在同一个模块中只能有一个
export default {
cat,
mouse,
}
3.2 import导入
/** module.js */
export var name = '张三'
export var age = 18
export default function () {
console.log('module.js')
}
/** main.js */
// 批量导入
import { name, age } from './module'
console.log(name, age) // 张三 18
// 默认导入
import fn from './module'
console.log(fn) // [Function: default]
// 混合导入
import fun, { name as Name, age as Age } from './module'
console.log(fun, Name, Age) // [Function: default] 张三 18
// 模块的整体加载
import * as all from './module'
console.log(all) // [Module: null prototype] { age: 18, default: [Function: default], name: '张三' }
3.3 重复导入
/** module.js */
console.log('module.js 执行了~')
export {
a: 10,
b: 20
}
/** main.js */
import './module' // module.js 执行了~
import './module' // 不会执行,没有输出内容
import { a } from './module'
import { b } from './module'
// 以上两行代码等同于 import { a, b } from './module'
console.log(a, b) // 10 20
3.4 动态导入
因为 export 和 import 需要处于模块顶部,并且是编译时加载,所以 import命令 无法实现动态导入,但在 ES2020提案 中引入了 import() 函数,支持了动态加载模块。
import() 返回一个 Promise 对象。
if (true) import xxx from 'XXX' // 报错
/** module.js */
export const name = 'module'
/** main.js */
const moduleName = './module.js'
if (true) {
import(moduleName).then(({ name }) => {
console.log(name) // module
})
}
3.5 导入值的变化
export 导出的是值的引用,并且是只读的,内部对值进行修改会同步到外部。不可以对引用值进行修改。
/** module.js */
var num = 0
var add = () => ++num
export { num, add }
/** main.js */
import { num, add } from './module'
console.log(num) // 0
add()
console.log(num) // 1
num = 30 // 报错
3.6 自动使用严格模式
由于 ESModule 模块自动使用严格模式,其 顶层this 执行 undefined。
/** module.js */
console.log(this)
/** main.js */
import './module' // undefined
3.7 ESModule 总结
- ESModule 会对加载结果进行缓存;
- ESModule 是编译时加载模块;
- ESModule 导出的是值的引用,并且只读;
- ESModule 会自动使用严格模式。
四、CJS和ESM的区别
- CJS 模块是通过 module.exports 和 exports 导出,require() 导入,ESM 模块是通过 export 导出,import 导入;
- CJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用;
- CJS 模块是运行时加载,ESM 模块是编译时输出接口;
- CJS 模块的
require()是同步加载模块,ESM 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段; - ESM 模块会自动使用严格模式,不管你有没有在模块头部添加
"use strict"。
参考文章
聊聊什么是CommonJs和Es Module及它们的区别 —— 蛙人 Module 的语法 - ECMAScript 6入门 —— 阮一峰