ES6 模块特性
ES6 模块跟 CommonJS 模块的不同,主要有以下两个方面:
- ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
- ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载
CommonJS 输出值的拷贝
CommonJS 模块输出的是值的拷贝(原始值的拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// a.js
var b = require('./b');
console.log(b.foo);
setTimeout(() => {
console.log(b.foo);
console.log(require('./b').foo);
}, 1000);
// b.js
let foo = 1;
setTimeout(() => {
foo = 2;
}, 500);
module.exports = {
foo: foo,
// foo: () => {
// return foo;
// },
};
// 执行:node a.js
// 执行结果:
// 1
// 1
// 1
上面代码说明,b 模块加载以后,它的内部 foo 变化就影响不到输出的 exports.foo 了, 所以如果你想要在 CommonJS 中动态获取模块中的值,那么就需要借助于函数延时执行的特性。
// b.js
module.exports = {
foo: () => {
return foo;
},
};
// 1
// 2
// 2
总结一下
- CommonJS 模块重复引入的模块并不会重复执行,再次获取模块直接获得暴露的 module.exports 对象
- 如果你要处处获取到模块内的最新值的话,也可以你每次更新数据的时候每次都要去更新 module.exports 上的值
- 如果你暴露的 module.exports 的属性是个对象,那就不存在这个问题了
// a.js
var b = require('./b');
console.log(b.foo);
setTimeout(() => {
console.log(b.foo);
console.log(require('./b').foo);
}, 1000);
// 每次更新数据的时候每次都要去更新 module.exports 上的值
// b.js
module.exports.foo = 1; // 同 exports.foo = 1
setTimeout(() => {
module.exports.foo = 2;
}, 500);
// 执行:node a.js
// 执行结果:
// 1
// 2
// 2
ES6 输出值的引用
然而在 ES6 模块中就不再是生成输出对象的拷贝,而是动态关联模块中的值。
// a.js
import { foo } from './b';
console.log(foo);
setTimeout(() => {
console.log(foo);
import('./b').then(({ foo }) => {
console.log(foo);
});
}, 1000);
// b.js
export let foo = 1;
setTimeout(() => {
foo = 2;
}, 500);
// 执行:babel-node a.js
// 执行结果:
// 1
// 2
// 2
ES6 静态编译,CommonJS 运行时加载
ES6 模块编译时执行会导致有以下两个特点:
- import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
- export 命令会有变量声明提前的效果。
import 优先执行:
// a.js
console.log('a.js')
import { foo } from './b';
// b.js
export let foo = 1;
console.log('b.js 先执行');
// 执行结果:
// b.js 先执行
// a.js
由于 import 是静态执行,所以 import 具有提升效果即 import 命令在模块中的位置并不影响程序的输出。
export 变量声明提升:
正常的引入模块是没办法看出变量声明提升的特性,需要通过循环依赖加载才能看出。
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}
// b.js
export let foo = 1;
import * as a from './a';
console.log(a);
// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
从上面的例子可以很直观地看出,a 模块引用了 b 模块,b 模块也引用了 a 模块,export 声明的变量也是优于模块其它内容的执行的,但是具体对变量赋值需要等到执行到相应代码的时候。(当然函数声明和表达式声明不一样,这一点跟 JS 函数性质一样,这里就不过多解释)
ES6 模块和 CommonJS 模块相同点
模块不会重复执行
这个很好理解,无论是 ES6 模块还是 CommonJS 模块,当你重复引入某个相同的模块时,模块只会执行一次。
// a.js
import './b';
import './b';
// b.js
console.log('只会执行一次');
// 执行结果:
// 只会执行一次
CommonJS 模块循环依赖
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');
// node a.js
// 执行结果:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
结合之前讲的特性很好理解,当你从 b 中想引入 a 模块的时候,因为 node 之前已经加载过 a 模块了,所以它不会再去重复执行 a 模块,而是直接去生成当前 a 模块吐出的 module.exports 对象,因为 a 模块引入 b 模块先于给 done 重新赋值,所以当前 a 模块中输出的 module.exports 中 done 的值仍为 false。而当 a 模块中输出 b 模块的 done 值的时候 b 模块已经执行完毕,所以 b 模块中的 done 值为 true
ES6 模块循环依赖
// a.js
console.log('a starting')
import {foo} from './b';
console.log('in b, foo:', foo);
export const bar = 2;
console.log('a done');
// b.js
console.log('b starting');
import {bar} from './a';
export const foo = 'foo';
console.log('in a, bar:', bar);
setTimeout(() => {
console.log('in a, setTimeout bar:', bar);
})
console.log('b done');
// babel-node a.js
// 执行结果:
// b starting
// in a, bar: undefined
// b done
// a starting
// in b, foo: foo
// a done
// in a, setTimeout bar: 2
动态 import()
ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码:
if(some condition) {
import a from './a';
}else {
import b from './b';
}
// or
import a from (str + 'b');
import() 允许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了 require.ensure 这个语法,但是它们的用途却截然不同的。
- require.ensure 的出现是 webpack 的产物,它是因为浏览器需要一种异步的机制可以用来异步加载模块,从而减少初始的加载文件的体积,所以如果在服务端的话 require.ensure 就无用武之地了,因为服务端不存在异步加载模块的情况,模块同步进行加载就可以满足使用场景了。 CommonJS 模块可以在运行时确认模块加载。
- 而 import() 则不同,它主要是为了解决 ES6 模块无法在运行时确定模块的引用关系,所以需要引入 import()
它的用法
- 动态的 import() 提供一个基于 Promise 的 API
- 动态的import() 可以在脚本的任何地方使用
- import() 接受字符串文字,你可以根据你的需要构造说明符
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}