前言
最近一个项目当中,写了好多个大大小小的模块,各种export、require等等的操作一开始搞不清的时候实在是太折磨人,趁着这个机会,好好总结一下。
用法
首先,先从结果导向弄清楚具体的用法。反正如果你封装的模块引用的时候是用”require“关键字,那么写这个模块的时候就需要使用”module.exports”或“exports“关键字把模块暴露出去。相反,如果你是使用”import“使用模块时,就需要使用”export”或“export default“导出。先记住这个用法,后面再谈谈有啥区别。
Node 模块
由于js这门语言设计得过于简单,根本没有”模块“的概念,因此Nodejs的诞生,需要拥有”模块“的话就需要遵守一个规范,这个规范就是CommonJS
CommonJS规范,规定了每个独立的js文件就是一个模块,模块与模块之间是完全解耦的,即在一个独立的模块里,所有的变量、函数、类,都是私有的。而 module 关键字就是代表这个模块,是一个对象。从阮一峰老师的博客里查看关于CommonJS规范说明,有这么一段描述:”module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量“
但事实上你可以导出任何东西,例如一个变量,一个函数,一个对象......
看以下一段代码:
// a.js
const a = 'a'
module.exports = a
// main.js
const a = require('./a.js')
console.log(a) // => a
// b.js
module.exports = (function() {
console.log('this is a function')
})()
// main.js
require('./b.js') // => this is a function
module.exports 和 exports 的区别
既然module.exports是nodejs导出(或者说暴露)封装好的模块的一个语法,那么exports和module.exports又有什么区别呢?
答案是大同小异,至于异在哪里,举一个例子,大家就很简单易懂了。对于导出函数而言,其实没有任何区别,上述b.js的例子中,用exports关键字导出,效果是一样的:
// b.js
exports = (function() {
console.log('this is a function')
})()
// main.js
require('./b.js') // => this is a function
但是对于导出变量或者对象的话,就有一些差异了。例如:
// a.js
const dog = {
name: 'nick',
type: 'labuladuo',
say: () => {
console.log('wong!! wong!!')
}
}
module.exports = dog
// main.js
const animal = require('./a.js')
console.log('animal = ', animal)
输出结果:
如果换成exports写法:
const dog = {
name: 'nick',
type: 'labuladuo',
say: () => {
console.log('wong!! wong!!')
}
}
exports.animal = dog // 注意:写成 exports = dog 是无效的,这样写的话,在应用中输出的animal是一个空对象
// main.js
const animal = require('./a.js')
console.log('animal = ', animal)
输出结果:
可以看出,相对于module.exports写法,外层多了一个{},换句话来说,用module.exports导出,实际应用中拿到的是我们在模块里封装好的对象,而用exports导出,需要拿到模块里的封装好的对象的话,就要这样写:
const animal = require('./a.js')
console.log('animal = ', animal.animal)
用导出变量再举一个例子,相信会清晰很多
// a.js
const a = 'aa'
const b = 'bb'
module.exports = a
// main.js
const obj = require('./a.js')
console.log('obj = ', obj) // => aa
// 如果要同时导出 a 和 b,则写成
// a.js
const a = 'aa'
const b = 'bb'
module.exports = { a, b }
// main.js
const obj = require('./a.js')
console.log('obj = ', obj) // => {a: 'aa', b: 'bb'}
如果用exports则可以写成:
// a.js
const a = 'aa'
const b = 'bb'
exports.a = a
exports.b = b
// main.js
const obj = require('./a.js')
console.log('obj = ', obj) // => {a: 'aa', b: 'bb'}
与上述的例子是一样的,不过如果只导出其中一个,对象里就只会有导出的那个变量
// a.js
const a = 'aa'
const b = 'bb'
exports.a = a
// exports.b = b
// main.js
const obj = require('./a.js')
console.log('obj = ', obj) // => {a: 'aa'}
写到这里,相信两者之间区别应该能搞懂了。
export 和 export default
对于这两者的差别,阮一峰老师的《# ECMAScript 6 入门》当中Module 的加载实现这一节有十分详细的描述,不过我知道你们不会看的,我就在这里就简单总结一下。
首先我们先用export导出一个简单的模块:
// a.js
var a = 'aa'
export { a } // 注意,这里必须加上 {},如果不加上 {},则需要写成 export var a = 'aa'
// main.js
import { a } from './a.js' // 引入的时候也需要加上 {}
console.log('a = ', a) // => a = aa
注意,在main.js中加载的时候,我们是必须要知道,我们需要加载的模块export出来的是什么。因此我们经常可以看到很多npm模块在github的.readme当中会写到具体用法,如:import moment from 'moment'
// a.js
var a = 'aa'
export { a }
// main.js
import { b } from './a.js'
console.log('a = ', b) // => a = undefined
这样在某程度上,可能对某些用户不太友好,ES6考虑到这种情况,因此有了export default的语法应对这种情况。
// a.js
var a = 'aa'
export default a
// main.js
import b from './a.js' // 注意,这里就不能加上 {} 了
console.log('a = ', b)
这样问题就解决了。
总结
require和import的意义差不多,都是加载第三方模块,只是import和export是ES6的语法,ES6模块与CommonJS模块的差异,主要有以下三点:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
第2点第3点应该比较好理解,而关于第1点,可能会有些同学比较疑惑,这里举一个例子大家就应该就清楚了。我们在很多情况下,都会使用JSON.parse(JSON.stringify(obj))深拷贝一个新的对象出来,以免影响原来的对象。同理,CommonJS 就类似于深拷贝一块新的内存,而import则是指向原来的内存块。这就很好理解,为什么 CommonJS 为什么只有运行时才加载,并且缓存下来,毕竟不可能每一次require都要占用一块新的内存空间。而import则是在编译阶段就已经加载了。