聊聊CommonJS与ES6 Module的那些事

667 阅读4分钟

随着前端工程越来越复杂,项目越来越大,现代前端开发时时刻刻都在和各种各样的模块打交道,在程序设计之初,便会根据功能不同将代码拆分成不同的片段,每一片段实现自己特定的使命,最后通过接口将分散的片段组合在一起,,也就是所谓的模块化开发。简单来说一个模块就是一个实现了特定功能的文件,大到项目中引入一个三方包,小到为某个功能单独提供一个JS工具函数文件,这些都可以称为模块。

为什么要采用模块化开发

早期JavaScript是不存在模块的概念的,面对多个JS文件只能通过<script>标签一个一个的插入到HTML中,这就导致如果各JS文件间存在依赖关系还需要确保引入顺序,同时还存在变量命名冲突的风险,而且如果<script>数量众多,每个JS文件都需要向服务器发送请求,那对页面加载性能无疑是灾难。

为了解决非模块开发下的诸多问题,从09年开始,JS社区陆续出现了以RequireJS为代表的AMD规范(Asynchromous Module Definition)、以SeaJS为代表的CMD规范(Common Module Definition)、还有NodeJS对应实现的CommonJS规范。

CommonJS

导入及导出

CommonJS中规定每一个文件就是一个独立的模块,各模块之间互不影响,每个模块都有独立的作用域。使用require进行模块导入,使用module.exports或exports对模块进行导出。

// A.js
// module.exports导出
module.exports = {
    test() {
        console.log('导出模块方式1')
    }
}
// exports导出
exports.test = function() {
    console.log('导出模块方式2')
}

// B.js
let a = require('./A.js)
a.test()

两种导出方式是等价的。内在机制大概就是module.exports原本是一个空对象,将需要导出的对象储存在module.exports中,exports内部指向了module.exports。 需要注意的是不能对exports重新赋值,否则会导致exports的指向改变,导致exports导出失败。

ES Module

2015年,ECMAScript6.0正式定义了JS模块标准,使得JS正式具备了模块这一特性。ES Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,使用import进行模块导入,使用export进行导出。

导入及导出

// A.js
// 普通导出
export function bar () {
    console.log('ES Module')
}

// 默认导出
let def = 'ES Module'
export default def

// B.js
// 导入
import { bar } from './A.js'
bar()

// 导入默认导出
import a from './A.js'
console.log(a)

// 批量导入
import * as c from './A.js'

CommonJS与ES Module的区别

对于普通的导入导出应该都是现代前端开发手到擒来的事情,而且面试中也没有人会去问如何导入导出模块,所以还是需要关注一下它们的区别

模块处理

在模块处理这一方面,CommonJS中模块依赖之间的关系建立发生在代码运行阶段,而ES Module发生在代码编译阶段。
在CommonJS中,A模块加载B模块时会执行B模块之中的代码,通过require导入的模块路径支持表达式,并且可以通过if进行条件判断是否加载某个模块,所以CommonJS模块在被执行前是没办法明确各依赖之间的关系。

// A.js 
module.exports = {
    test: '测试'
}

// B.js
let path = 'A.js'
let test 
if (true) {
    test = require('./' + path)
}
console.log(test)

而在ES Module中,模块在编译阶段就可以分析出模块之间的依赖关系,不支持导入的路径是一个表达式,并且导出语句必须写在模块的顶层区域。

导出值的区别

在ES Module中,导入的值是对原模块导出值的引用,通过下面例子就可以验证,当通过add方法改变原A模块中num的值时,B模块中num的值随着A模块中num的值的改变而改变了。

// A.js
let num = 1
function add () {
    num ++
    console.log('A模块中num的值: ' + num)
}
export {
    num,
    add
}

// B.js
import { num, add } from "./A.js";
console.log('导入时num的值:' + num)
add()
console.log('调用add方法后num的值:' + num)

// 导入时num的值:1
// A模块中num的值: 2
// 调用add方法后num的值:2

在CommonJS中,导入的值时原模块导出值的拷贝,B模块中num的值并没有随着A模块中值的改变而改变。

// A.js
let num = 1
function add () {
    num ++
    console.log('A模块中num的值: ' + num)
}
module.exports = {
    num,
    add
}

// B.js
let obj = require('./A.js')
console.log('导入时num的值:' + obj.num)
obj.add()
console.log('调用add方法后num的值:' + obj.num)

// 导入时num的值:1
// A模块中num的值: 2
// 调用add方法后num的值:1

总结

ES Module作为官方推出的JS模块标准,也算是明确了JS的模块化发展方向,而社区规范CommonJS规范的也在NodeJS这个顶级实践者中混的风生水起,不过目前NodeJS正在支持ES Module的模块化实现方式,可以通过配置package.json中"type": "module"或将文件后缀改为.mjs实现ES Module的导入导出