moduleexports和exports区别
Node 应用是有模块组成的,一个文件就是一个模块,有单独的作用域,文件中声明的变量、方法和类都是私有的,如果需要在外部能访问,则需要使用 module.exports 或则 exports 暴露出去。这里 Node 采用的是 CommonJS 模块规范。
# 一、模块化
提到 CommonJS 模块化,自然就要说起 ES Module 模块化规范。为什么需要模块化规范呢?
早期 JavaScript 开发很容易存在全局污染和依赖管理混乱问题,这些问题在多人开发前端应用的情况下变得更加棘手。
# 二、CommonJS
Commonjs 的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。nodejs 借鉴了 Commonjs 的 Module ,实现了良好的模块化管理。
目前 commonjs 广泛应用于以下几个场景:
- Node 是 CommonJS 在服务器端一个具有代表性的实现;
- Browserify 是 CommonJS 在浏览器中的一种实现;
- Webpack 打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。
# commonjs 使用与原理
在使用规范下,有几个显著的特点:
- 在 commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;
- 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;
- exports 和 module.exports 可以负责对模块中的内容进行导出;
- require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
# exports 导出
exports 本质是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出。
bar.js 文件导出:
const name = "simonzhangs";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
2
3
4
5
6
7
8
9
10
main.js 文件导入:
const bar = require("./bar");
console.log(bar.name); // simonzhangs
console.log(bar.age); // 18
2
3
4
在这里,main.js 中的 bar 变量等于 exports 对象;所以我们通过 bar.xxx 来使用导出文件内的变量,比如 name,age;
require 其实是一个函数,返回值是一个对象,值为导出文件的 exports 对象。
从内存角度分析,在 node 中有一个特殊的全局变量,exports 是其中一个。如果在文件内,不使用 exports.xxx 的形式导出某个变量的话,其实 exports 就是一个空对象。
- 当我们在 main.js 中 require 导入的时候,它会去自动查找特殊的全局对象 exports,并且把 require 函数的执行结果赋值给 bar;
- bar 和 exports 指向同一个引用(引用地址相同);
- 如果发现 exports 上有变量,则会放到 bar 对象上,正因为这样我们才能从 bar 上读取想用的变量;
提示
CommonJS 规范的本质就是对象的引用赋值(浅拷贝本质)。
# module.exports
CommonJS 中是没有 module.exports 的概念的;
- 但是为了实现模块的导出,Node 中使用的是 Module 的类,每一个模块都是 Module 的一个实例 module;
- 所以在 Node 中真正用于导出的其实根本不是 exports,而是 module.exports;
- exports 只是 module 上的一个对象。
但是为什么 exports 也可以导出呢?
因为 module 对象的 exports 属性是 exports 对象的一个引用。真正导出的模块内容的核心其实是 module.exports,只是为了实现 CommonJS 的规范,刚好 module.exports 对 exports 对象使用的是同一个引用而已。
如果使用 module.exports 后,对 exports 有什么影响呢?
如果使用 module.exports = { xxx }这样的形式,会在堆内存中开辟出一块内存空间,会生成一个新的对象,用它取代之前的 exports 对象的导出。
# require 模块的加载顺序
- 模块在被第一次引入时,模块中的 js 代码会被运行一次;
- 模块被多次引入时,会缓存,最终只加载(运行)一次;
为什么只会加载运行一次呢?
因为每个模块对象 module 都有一个属性,loaded:为 false 表示还没有加载,为 true 表示已经加载。
- 如果有循环引入,那么加载顺序是按照图结构中的深度优先算法。
# 三、ES Module
Nodejs 借鉴了 Commonjs 实现了模块化 ,从 ES6 开始, JavaScript 才真正意义上有自己的模块化规范, Es Module 的产生有很多优势,比如:
借助 Es Module 的静态导入导出的优势,实现了 tree shaking。 Es Module 还可以 import() 懒加载方式实现代码分割。
在 Es Module 中用 export 用来导出模块,import 用来导入模块。但是 export 配合 import 会有很多种组合情况
# 导出 export 和导入 import
# 1. export 正常导出,import 导入
导出模块:a.js
const name = " ";
const author = " ";
export { name, author };
export const say = function () {
console.log("hello , world");
};
2
3
4
5
6
导入模块:main.js
// name , author , say 对应 a.js 中的 name , author , say
import { name, author, say } from "./a.js";
2
- export { }, 与变量名绑定,命名导出。
- import { } from 'module', 导入 module 的命名导出 ,module 为如上的 ./a.js
- 这种情况下 import { } 内部的变量名称,要与 export { } 完全匹配。
# 2. 默认导出 export default
导出模块:a.js
const name = " ";
const author = " ";
const say = function () {
console.log("hello , world");
};
export default {
name,
author,
say,
};
2
3
4
5
6
7
8
9
10
导入模块:main.js
import mes from "./a.js";
console.log(mes); //{ name: ' ',author:' ', say:Function }
2
- export default anything 导入 module 的默认导出。 anything 可以是函数,属性方法,或者对象。
- 对于引入默认导出的模块,import anyName from 'module', anyName 可以是自定义名称。
# 3. 混合导入|导出
ES6 module 可以使用 export default 和 export 导入多个属性。 导出模块:a.js
export const name = " ";
export const author = " ";
export default function say() {
console.log("hello , world");
}
2
3
4
5
6
导入模块:main.js 中有几种导入方式: 第一种:
import theSay, { name, author as bookAuthor } from "./a.js";
console.log(
theSay, // ƒ say() {console.log('hello , world') }
name, // " "
bookAuthor // " "
);
2
3
4
5
6
第二种:
import theSay, * as mes from "./a";
console.log(
theSay, // ƒ say() { console.log('hello , world') }
mes // { name:' ' , author: " " ,default: ƒ say() { console.log('hello , world') } }
);
2
3
4
5
导出的属性被合并到 mes 属性上, export 被导入到对应的属性上,export default 导出内容被绑定到 default 属性上。 theSay 也可以作为被 export default 导出属性。
# ES Module 特性
# 1. 静态语法
ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 ,import , export 不能放在块级作用域或条件语句中。
# 2. 执行特性
ES6 module 和 Common.js 一样,对于相同的 js 文件,会保存静态属性。
但是与 Common.js 不同的是 ,CommonJS 模块同步加载并执行模块文件,ES6 模块提前加载并执行模块文件,ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 -> 父。
<code-block title="main.js" active>
```js
console.log('main.js开始执行')
import say from './a'
import say1 from './b'
console.log('main.js执行完毕')
```
</code-block>
<code-block title="a.js">
```js
import b from './b'
console.log('a模块加载')
export default function say (){
console.log('hello , world')
}
```
</code-block>
<code-block title="b.js">
```js
console.log('b模块加载')
export default function sayhello(){
console.log('hello,world')
}
```
</code-block>
// Make sure to add code blocks to your code group
main.js 和 a.js 都引用了 b.js 模块,但是 b 模块也只加载了一次。 执行顺序是子 -> 父:
b模块加载
a模块加载
main.js开始执行
main.js执行完毕
2
3
4
# 3. 导出绑定
不能修改 import 导入的属性。
- 使用 import 被导入的模块运行在严格模式下。
- 使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值
- 使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。
# 4. import()动态导入
import() 返回一个 Promise 对象, 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。
- import()来动态加载
import() 动态加载一些内容,可以放在条件语句或者函数执行上下文中。
if (isRequire) {
const result = import("./b");
}
2
3
- import()懒加载:
vue 中的路由懒加载;
[
{
path: "home",
name: "首页",
component: () => import("./home"),
},
];
2
3
4
5
6
7
参考文章: