require/exports 和 import/export 的区别
介绍
require/exports
require/exports 出生在野生规范当中,什么叫做野生规范?即这些规范是 JavaScript 社区中的开发者自己草拟的规则,得到了大家的承认或者广泛的应用。比如 CommonJS、AMD、CMD 等等。
AMD、CMD规范,2014年已基本上不怎么使用了,CommonJS 作为 Node.js 的规范,一直沿用至今。
require/exports 遵循的就是 CommonJS 规范,所以在 NodeJs 中可以直接使用。
写法
输出:
exports.fs = fs
module.exports = fs
引用:
const fs = require('fs')
import/export
import/export 则是遵循的名门正派 ES6 规范。
Node.js 无法直接兼容 ES6,
node8 ,已经支持 es6 绝大多数特性和 es7 的绝大多数特性,就是没有 import。
那我们为何还能在项目中使用 ES6 新特性呢?
通过 babel 将还未被宿主环境(各浏览器、Node.js)直接支持的 ES6 Module 编译为 ES5 的 CommonJS【Webpack中的babel-loader】
写法
输出和引用:
// 有关键字 default
export default fs;
import fs from 'fs';
// 直接 export
export const fs;
export function readFile;
export { readFile, read };
export * from 'fs';
import { default as fs } from 'fs';
import * as fs from 'fs'; // 导入所有,并命名为 fs
import { readFile } from 'fs';
import { readFile as read } from 'fs'; // 导入fs模块中的readFile,并命名为 read
import fs, { readFile } from 'fs';
export default 和 export 的区别
- export与export default均可用于导出常量、函数、文件、模块等
- 可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要
- 其实很多时候export与export default可以实现同样的目的,只是用法有些区别。注意第四条,通过export方式导出,在导入时要加{ },export default则不需要。使用export default命令,为模块指定默认输出,这样就不需要知道所要加载模块的变量名
注意以下代码的区别
export default const a = 1 // 错误
const a = 1
export default a // 正确
export const a = 1 // 正确
require/exports 和 import/export 的区别
- 遵循的规范不同【见上文】
- 调用时间不同、书写位置不同:
- require是运行时调用,所以require理论上可以运用在代码的任何地方
- import是编译时调用,所以必须放在文件开头
- 编译方式:import静态编译,import的地址不能通过计算,require就可以,例如
const url = "a" + "b";
, Import url 直接报错了,require(url)不会报错,所以require都会用在动态加载的时候。 - 加载:require 运行时加载(即动态加载),import 在编译时加载(即静态加载)
- 导入:require 导入整个模块对象,不能仅导入部分内容,import 可以导入模块中的所有导出内容或者部分导出内容
- 导出:require 的 module.export 之后,导出的值就不能再变化(输出值的拷贝),import 的 export之后导出的值可以变化(输出值的映射)
本质上是 ES6 模块与 CommonJS 模块的差异【见下文】
ES6 模块与 CommonJS 模块的差异 (阮老师的文章 ECMAScript 6入门 )
- CommonJS 模块输出的是一个值的拷贝【模块外和模块内的值不再有关系】,ES6 模块输出的是值的引用【模块内部的值改变,外部也会改变】。
- CommonJS 模块是运行时加载,输出的是一个 exports 对象。 ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。
$ node main.js
3
4
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
还是举上面的例子。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。
再举一个出现在export一节中的例子。
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。
让我们看看,m2.js能否正确读取这个变化。
$ babel-node m2.js
bar
baz
上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代码中,main.js从lib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量。
最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例。
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
现在执行main.js,输出的是1。
$ babel-node main.js
1
这就证明了x.js和y.js加载的都是C的同一个实例。