【JS基础】ES6模块系统

86 阅读4分钟

参考zhuanlan.zhihu.com/p/33843378

1、es6模块特性

ES6 模块跟 CommonJS 模块不同,主要有以下两个方面:

  1. ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
  2. ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载

特性1:输出值的引用

也就是说,在commonjs中,在不同的模块中引入同一个值,是不会相互影响的。而在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

特性2:静态编译

ES6 模块编译时执行会导致有以下两个特点:

  1. import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
  2. export 命令会有变量声明提前的效果。

特性3:模块不会重复执行

无论是 ES6 模块还是 CommonJS 模块,当你重复引入某个相同的模块时,模块只会执行一次。

// a.js
import './b';
import './b';

// b.js
console.log('只会执行一次');

// 执行结果:
// 只会执行一次

特性4:循环依赖

ES6 不会再去执行重复加载的模块,又由于 ES6 动态输出绑定的特性,能保证 ES6 在任何时候都能获取其它模块当前的最新值。

注意这里有一个自己之前不知道的点:当我们从某个js里import的时候,也会执行这个js文件里的代码,比如js文件里有立即执行的代码,就像下边这个例子里的console.log()

// 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

特性5:动态import

ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码:

if(some condition) {
  import a from './a';
}else {
  import b from './b';
}

// or 
import a from (str + 'b');

因为编译时静态分析,导致了我们无法在条件语句或者拼接字符串模块,因为这些都是需要在运行时才能确定的结果在 ES6 模块是不被允许的,所以 动态引入 import() 应运而生。动态 import() 为我们提供了以异步方式使用 ES 模块的额外功能,可以 根据我们的需求动态或有条件地加载它们。

我们先来看下它的用法:

  1. 动态的 import() 提供一个基于 Promise 的 API
  2. 动态的import() 可以在脚本的任何地方使用
  3. import() 接受字符串文字,你可以根据你的需要构造说明符
// a.js
const str = './b';
const flag = true;
if(flag) {
  import('./b').then(({foo}) => {
    console.log(foo);
  })
}
import(str).then(({foo}) => {
  console.log(foo);
})

// b.js
export const foo = 'foo';

// babel-node a.js
// 执行结果
// foo
// foo

2、模块常见用法

export

export 导出方式有两种,命名导出默认导出

  • 命名导出还是默认导出都是导出模块中内容的一种方式,可以混合使用。
  • 个人理解:默认导出其实是导出了default别名变量。
  • 一个模块只能有一个默认导出
  • 不同的导出方式也对应了不同的导入方式
// 命名行内导出
export const baz = 'baz';
export const foo = 'foo', bar = 'bar';
export function foo() {}
export function* foo() {}
export class Foo {}
// 命名子句导出
export { foo };
export { foo, bar };
export { foo as myFoo, bar };
// 默认导出
export default 'foo';
export default 123;
export default /[a-z]*/;
export default { foo: 'foo' };
export { foo, bar as default };
export default foo
export default function() {}
export default function foo() {}
export default function*() {}
export default class {}

import

  • 导入对模块而言是只读的,相当于const 变量
  • import导入的值是无法直接修改的,但可以修改导入对象的属性。
import foo, * as Foo './foo.js';
foo = 'foo'; // 错误
Foo.foo = 'foo'; // 错误
foo.bar = 'bar'; // 允许
  • 对应不同的导出方式,需要使用不同的导入方式:
// export.js
const foo = 'foo', bar = 'bar', baz = 'baz';
export { foo, bar, baz }
export default foo


// import .js
import * as Foo from './foo.js'; // 导入命名导出内容
import { foo, bar, baz as myBaz } from './foo.js';  // 导入命名导出内
// 导入默认导出的内容
import { default as foo } from './foo.js';//导入默认导出,与下边等效
import foo from './foo.js';//导入默认导出,与上边边等效
  • 如果模块同时使用了命名导出和默认导出,可以这样来在import中同时导入
import foo, { bar, baz } from './foo.js';
import { default as foo, bar, baz } from './foo.js';
import foo, * as Foo from './foo.js';

模块转移导出

// 导出foo.js所有命名导出
export * from './foo.js';
// 将foo.js中默认导出修改为命名导出
export {default as foo} from './foo.js'

在组件库或函数库中,我们经常能看到模块转移导出,将需要导出到外部的内容有一个统一的出口,这时要注意导出名称是否会重名等问题