exports、module.exports 和 export、export default

exports、module.exports 和 export、export default

JavaScript 模块化编程的模块引入上,主要有两种方式,如下

  • CommonJS 模块标准
  • ES6 moduel 特性

但是我们需要注意的是,CommonJS 模块规范和 ES6 模块规范完全是两种不同的概念,下面我们就来看看他们之间的区别

CommonJS 模块规范

Node.js 应用由模块组成,它采用了 CommonJS 模块规范,根据这个规范,每个文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见的,CommonJS 规范规定,每个模块内部,module 变量代表当前模块,这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口,加载某个模块,其实是加载该模块的 module.exports 属性,如下

1
2
3
4
5
6
7
8
9
var x = 5

var addX = function (value) {
return value + x
}

module.exports.x = x

module.exports.addX = addX

上面代码通过 module.exports 输出变量 x 和函数 addX,而我们使用 require 方法就可以来加载该模块

1
2
3
4
var example = require('./example.js')

console.log(example.x) // 5
console.log(example.addX(1)) // 6

exports 与 module.exports

我们在一般开发过程当中导出模块的方式有两种方式,第一种就是直接对 module.exports 赋值,这个也是使用较多的

1
2
3
4
module.exports = {
x: x,
addX: addX
}

或者也可以直接使用 exports,即直接在该对象上添加方法,表示对外输出的接口,如同在 module.exports 上添加一样

1
exports.addX = addX

但是针对上面的第一种用法,你不可以直接对 exports 赋值,就像下面这样

1
2
3
4
5
// 错误的使用方式
exports = {
x: x,
addX: addX
}

代码虽然可以执行,但是模块并没有输出任何变量,这里就需要注意了,是不能直接将 exports 变量指向一个值的,因为这样操作就等于切断了 exportsmodule.exports 之间的联系了

那么这里就存在一个问题了,exportsmodule.exports 到底有什么区别和联系呢?这里就要提起 require() 这个方法了,在官方 API 当中,有一个经典的例子就是把 require 方法简化成了下面这个函数

1
2
3
4
5
6
7
8
9
10
function require() {
// ...
function (module, exports) {
// Your module code here
exports = some_func // re-assigns exports, exports is no longer
// a shortcut, and nothing is exported.
module.exports = some_func // makes your module export 0
} (module, module.exports)
return module
}

通过观察传入进去的参数,我们可以看到 exports = module.exports,所以说,一开始这两个东西是指向同一个对象实例的(实际上是同一个变量,并且初始化为空对象 { } ),如下图所示

如果直接添加一个方法,比如 exports.fn = func,那么由于 exports 指向 module.exports,由于复制引用的关系,相当于 module.exports 也添加了同样的方法,类似下图

1
2
3
4
5
6
// 在 exports 上添加了一个方法,module.exports 也添加了同样的方法,因为它们指向了同样的对象实例  
exports.fn = function () {
console.log(`hello world!`)
}

module.exports.fn() // hello world!

如果给 exports 直接赋值,比如这里 exports = some_func,那么 exports 的指向就变了,但最后导出的是 module,因此,exports 指向的方法或者今后在 exports 上添加的方法都不会影响到 module.exports,因此不会被导出

引用官方 API 的一句话

As a guideline, if the relationship between exports and module.exports seems like magic to you, ignore exports and only use module.exports.

所以还是建议尽量使用 module.exports,但是在有的时候,比如我们要输出的是一个函数或者数组,那么,只能给 module.exports 赋值

1
module.exports = function () { return 'foo' }

exports 赋值是无效的,因为赋值后,module.exports 仍然是空对象 {} (因为最后导出的是 module),我们可以简单的总结一下

  • 如果要输出一个键值对象 {},可以利用 exports 这个已存在的空对象 {},并继续在上面添加新的键值
  • 如果要输出一个函数或数组,必须直接对 module.exports 对象赋值

所以我们可以得出结论,直接对 module.exports 赋值,可以应对任何情况

1
2
3
module.exports = {
foo: function () { return 'foo' }
}

或者

1
module.exports = function () { return 'foo' }

总之,尽量使用 module.exports 来输出模块变量

ES6 模块规范

在看完了 CommonJS 模块规范之后,我们再来看看 ES6 当中的模块规范,不同于 CommonJSES6 使用 exportimport 来导出、导入模块,在 ES6 模块当中比较容易混淆的也就是 exportexport default 以及它们对应的引入方式,我们先来看看它们的区别,如下

  • exportexport default 均可用于导出常量、函数、文件、模块等
  • 在一个文件或模块中 exportimport 可以有多个,但是 export default 仅有一个
  • 通过 export 方式导出,在导入时要加 { }export default 则不需要
  • export 能直接导出变量表达式,export default 不行

我们先来看看如何进行导出,先来看看通常的 export 导出方式

1
2
3
4
5
6
7
// 导出变量
export const a = '100'

// 导出方法
export const say = function () {
console.log(`hello`)
}

export default 导出方式如下,这里需要注意,不能写成 export defult const m = 100 这样的格式

1
2
3
const m = 100

export default m

另外还有一种使用较少的导出方式

1
2
3
4
5
function say() {
console.log(`hello`)
}

export { say }

我们假设上方代码是写在 test.js 当中的,引入方式如下

1
2
3
4
5
import { say } from './test'          // 导出了 export 方法 

import m from './test' // 导出了 export default

import * as testModule from './test' // as 集合成对象导出

区别

最后我们再来简单的看看它们两者之间的区别,如下所说

  • CommonJS 模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  • CommonJS 是单个值导出,ES6 Module 可以导出多个
  • CommonJS 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  • CommonJSthis 是当前模块,ES6 Modulethisundefined

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×