本章内容
理解模块模式
凑合的模块系统
使用前ES6模块加载器
使用ES6模块
可能出现的面试题:
1.ES6模块的基本使用方法
2.判断export和import语句是否合法的程序题
凑合的模块系统 略
ES6之前的模块加载器 略
知识点:
1.理解模块模式
把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。
模块标识符
模块标识符是所有模块系统通用的概念。
模块依赖
模块系统的核心是管理依赖。指定依赖的模块与周围的环境会达成一种契约。本地模块向模块系统声明一组外部模块(依赖),这些外部模块对于当前模块正常运行是必需的。模块系统检视这些依赖,进而保证这
些外部模块能够被加载并在本地模块运行时初始化所有依赖。
模块加载
加载模块的概念派生自依赖契约。当一个外部模块被指定为依赖时,本地模块期望在执行它时,依赖已准备好并已初始化。
加载模块涉及执行其中的代码,但必须是在所有依赖都加载并执行之后。
入口
相互依赖的模块必须指定一个模块作为入口(entry point),这也是代码执行的起点。
模块加载是“阻塞的”,这意味着前置操作必须完成才能执行后续操作。每个模块在自己的代码到达浏览器之后完成加载,此时其依赖已经加载并初始化。
2.异步依赖
因为JavaScript可以异步执行,所以如果能按需加载就好了。换句话说,可以让JavaScript通知模块系统在必要时加载新模块,并在模块加载完成后提供回调。
动态依赖
有些模块系统要求开发者在模块开始列出所有依赖,而有些模块系统则允许开发者在程序结构中动态添加依赖。
if (loadCondition) {
require('./moduleA');
}
在这个模块中,是否加载moduleA 是运行时确定的。加载moduleA 时可能是阻塞的,也可能导致执行,且只有模块加载后才会继续。无论怎样,模块内部的代码在moduleA 加载前都不能执行,因为moduleA 的存在是后续模块行为正确的关键。
3.循环依赖
模块之间相互依赖,但是指定了入口后,每个依赖都只被加载一次。
4.使用ES6模块
模块标签及定义
ECMAScript 6模块是作为一整块JavaScript代码而存在的。带有type=“module” 属性的 标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。模块可以嵌入在网页中,也可以作为外部文件引入:
<script type="module">
// 模块代码
</script>
<script type="module" src="path/to/myModule.js"></script>
与传统脚本不同,所有模块都会像 加载的脚本一样按顺序执行。解析到
标签后会立即下载模块文件,但执行会延迟到文档解析完成。无论对嵌入的模块代码,还是引入的外部模块文件,都是这样。
在页面中
出现的顺序就是它们执行的顺序。与 一样,修改模块标签的位置,无论是在
还是在
中,只会影响文件什么时
候加载,而不会影响模块什么时候加载。
<!-- 第二个执行 -->
<script type="module"></script>
<!-- 第三个执行 -->
<script type="module"></script>
<!-- 第一个执行 -->
<script></script>
<!-- 第二个执行 -->
<script type="module" src="module.js"></script>
<!-- 第三个执行 -->
<script type="module" src="module.js"></script>
<!-- 第一个执行 -->
<script><script>
也可以给模块标签添加async 属性。这样影响就是双重的:不仅模块执行顺序不再与 标签在页面中的顺序绑定,模块也不会等待文档完成解析才执行。不过,入口模块仍必须等待其依赖加载完成。
5.模块行为
模块代码只在加载后执行。
模块只能加载一次。
模块是单例。
模块可以定义公共接口,其他模块可以基于这个公共接口观察和交
互。
模块可以请求加载其他模块。
支持循环依赖。
ES6模块系统也增加了一些新行为。
ES6模块默认在严格模式下执行。
ES6模块不共享全局命名空间。
模块顶级this 的值是undefined (常规脚本中是window )。
模块中的var 声明不会添加到window 对象。
ES6模块是异步加载和执行的。
6.模块导出
ES6模块支持两种导出:命名导出和默认导出。
export 关键字
// 允许
export ...
// 不允许
if (condition) {
export ...
}
export 语句甚至可以出现在它要导出的值之前:
// 允许
const foo = 'foo';
export {
foo };
// 允许
export const foo = 'foo';
// 允许,但应该避免
export {
foo };
const foo = 'foo';
命名导出 (named export)就好像模块是被导出值的容器。
export const foo = 'foo';
变量声明跟导出可以不在一行。可以在export 子句中执行声明并将标识符导出到模块的其他地方:
const foo = 'foo';
export {
foo };
导出时也可以提供别名,别名必须在export 子句的大括号语法中指定。因此,声明值、导出值和为导出值提供别名不能在一行完成。在下面的例子中,导入这个模块的外部模块可以使用myFoo 访问导出的值:
const foo = 'foo';
export {
foo as myFoo };
const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
export {
foo, bar as myBar, baz };
默认导出 (default export)就好像模块与被导出的值是一回事。默认导出使用default 关键字将一个值声明为默认导出,每个模块只能有一个默认导出。重复的默认导出会导致SyntaxError 。
const foo = 'foo';
export default foo;
const foo = 'foo';
const bar = 'bar';
export {
bar };
export default foo;
const foo = 'foo';
const bar = 'bar';
export {
foo as default, bar };
例子:
// 命名行内导出
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 {
}
// 会导致错误的不同形式:
// 行内默认导出中不能出现变量声明
export default const foo = 'bar';
// 只有标识符可以出现在export子句中
export {
123 as foo }
// 别名只能在export子句中出现
export const foo = 'foo' as myFoo;
7.模块导入
// 允许
import ...
// 不允许
if (condition) {
import ...
}
// 允许
import {
foo } from './fooModule.js';
console.log(foo); // 'foo'
// 允许,但应该避免
console.log(foo); // 'foo'
import {
foo } from './fooModule.js';
// 解析为/components/bar.js
import ... from './bar.js';
// 解析为/bar.js
import ... from '../bar.js';
// 解析为/bar.js
import ... from '/bar.js';
导入对模块而言是只读的,实际上相当于const 声明的变量。在使用*执行批量导入时,赋值给别名的命名导出就好像使用Object.freeze() 冻结过一样。直接修改导出的值是不可能的,但可以修改导出对象的属性。同样,也不能给导出的集合添加或删除导出的属性。
要修改导出的值,必须使用有内部变量和属性访问权限的导出方法。
import foo, * as Foo './foo.js';
foo = 'foo'; // 错误
Foo.foo = 'foo'; // 错误
foo.bar = 'bar'; // 允许
命名导出和默认导出的区别也反映在它们的导入上。
命名导出可以使用* 批量获取并赋值给保存导出集合的别名,而无须列出每个标识符:
const foo = 'foo', bar = 'bar', baz = 'baz';
export {
foo, bar, baz }
import * as Foo from './foo.js';
console.log(Foo.foo); // foo
console.log(Foo.bar); // bar
console.log(Foo.baz); // baz
默认导出就好像整个模块就是导出的值一样。可以使用default 关键字并提供别名来导入。也可以不使用大括号,此时指定的标识符就是默认导出的别名:
// 等效
import {
default as foo } from './foo.js';
import foo from './foo.js';
8.模块转移导出
如果想把一个模块的所有命名导出集中在
一块,可以像下面这样在bar.js中使用* 导出:
foo.js
export const baz = 'origin:foo';
bar.js
export * from './foo.js';
export const baz = 'origin:bar';
main.js
import {
baz } from './bar.js';
console.log(baz); // origin:bar
此外也可以明确列出要从外部模块转移本地导出的值。该语法支持使用别名:
export {
foo, bar as myBar } from './foo.js';
类似地,外部模块的默认导出可以重用为当前模块的默认导出:
export {
default } from './foo.js';
这样不会复制导出的值,只是把导入的引用传给了原始模块。在原始模块中,导入的值仍然是可用的,与修改导入相关的限制也适用于再次导出的导入。
在重新导出时,还可以在导入模块修改命名或默认导出的角色。比如,可以像下面这样将命名导出指定为默认导出:
export {
foo as default } from './foo.js';
工作者模块略