总结一下exports、module.exports、export、export default等用法

272 阅读5分钟

前言

最近一个项目当中,写了好多个大大小小的模块,各种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导出(或者说暴露)封装好的模块的一个语法,那么exportsmodule.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)

输出结果:

image.png

如果换成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)

输出结果:

image.png

可以看出,相对于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)

这样问题就解决了。

总结

requireimport的意义差不多,都是加载第三方模块,只是importexport是ES6的语法,ES6模块与CommonJS模块的差异,主要有以下三点:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

第2点第3点应该比较好理解,而关于第1点,可能会有些同学比较疑惑,这里举一个例子大家就应该就清楚了。我们在很多情况下,都会使用JSON.parse(JSON.stringify(obj))深拷贝一个新的对象出来,以免影响原来的对象。同理,CommonJS 就类似于深拷贝一块新的内存,而import则是指向原来的内存块。这就很好理解,为什么 CommonJS 为什么只有运行时才加载,并且缓存下来,毕竟不可能每一次require都要占用一块新的内存空间。而import则是在编译阶段就已经加载了。