第十三章 用模块封装代码
模块是自动运行在严格模式下并且没有办法退出运行的JavaScript代码,在模块顶部创建的变量不会自动被添加到全局共享作用域,这个变量尽在模块的顶级作用域中存在,而且模块必须到处一些外部代码可以访问的元素,如变量或函数。
- 在模块的顶部,
this
是undefined
。
导出的基本语法
// 导出数据
export var color = "red";
export let name = "NowhereToRun";
export const MAGICNUMBER = 7;
// 导出函数
export function sum(num1, num2) {
return num1 + num2
}
// 导出类
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// 这个函数是模块私有的
function subtract(num1, num2) {
return num1 - num2;
}
// 定义一个函数 之后将它导出 重命名,使用as指定函数在模块外的名称
function multiply(num1, num2) {
return num1 * num2
};
export { multiply as mul };
导入的基本语法
import {identifier1, identifier2} from "./example.js"
浏览器使用的路径与传给<script>元素的相同,也就是说,必须把文件扩展名也加上。
Node.js则遵循基于文件系统前缀区分本地文件和包的惯例。例如,example是一个包而./example.js是一个本地文件。
当从模块中导入一个绑定时,它就好像使用const定义的一样。结果是你无法定义另一个同名变量,也无法在import语句前使用标识符或改变绑定的值。
import { sum, multiply } from "./test" // 多个导入 同样的方法可以单个导入
import * as thatModule from "./test" // 全部导入
不管import语句中把一个模块写了多少次,该模块将只执行一次。
导入模块的代码执行后,实例化过的模块被保存在内存中,只要一个import语句引用它就可以重复使用它。
模块语法的限制
export和import的一个重要的限制是,他们必须在其他语句和函数之外使用。例如,下面代码会给出一个语法错误:
var flag = 1;
if(flag){
export {flag}
}
// SyntaxError: src/test.js: 'import' and 'export' may only appear at the top level
同样,不能再一条语句中使用import,必须在顶部使用它。
因为:
模块语法存在的一个原因是要让JavaScript引擎静态地确定哪些可以导出或导入。export和import关键字被设计成静态的,因而像文本编辑器这样的工具可以轻松地识别模块中哪些信息时可用的。
导入绑定的一个微妙怪异之处
ES6的import语句为变量、函数和类创建的是只读绑定,而不是像正常变量一样简单地引用原始绑定。标识符只有在被导出的模块中可以修改,即便是导入绑定的模块也无法更改绑定的值 。
// test.js
export var color = "red";
export function setColor(){
color = "blue"
}
/****************/
// module2
import { color, setColor } from "./test"
console.log(color);
setColor('yello');
console.log(color);
color = "blue" // SyntaxError: src/test2.js: "color" is read-only
调用setColor
会回到导出setColor()的模块中去执行。请注意,此更改会自动在导入的color绑定上体现,其原因是,color是导出的color标识符的本地名称。本段代码中使用的color和模块中导入的color不是同一个。(???)
模块的默认值
在诸如CommonJS的其他模块系统中,从模块中导出和导入默认值是一个常见的做法,该语法被进行了优化。模块的默认值值得是通过default
关键字指定的单个变量、函数或类,只能为每个模块设置一个默认的导出值。
导出默认值
export default function (num1, num2) {
return num1 + num2
}
/*****或者****/
function sum(num1, num2) {
return num1 + num2
}
export default sum;
此外,在重命名导出时标识符default
具有特殊含义,用来指示模块的默认值。由于default
是JavaScript中的默认关键字,因此不能将其用于变量、函数或类的名称。但是,可以将其用作属性名称。所以用default来重命名模块是为了尽可能与非默认导出的定义一致。如果想在一条导出语句中同时指定多个导出(包括默认导出),这个语法非常有用。
function sum(num1, num2) {
return num1 + num2
}
export {sum as default};
导入默认值
import aaa from "./test"
console.log(aaa(1,2));
这条import语句从模块test中导入了默认值,请注意这里没有使用大括号,与非默认导入的情况不同。本地名称aaa表示,模块导出的任何默认函数,这种语法是最纯净的,ES6的创建者希望它能够成为Web上主流的模块导入形式。
对于导出默认值和一或多个非默认绑定的模块,可以用一条语句导入所有导出的绑定。例如有下面这个模块
export let color = "red";
export default function (num1, num2) {
return num1 + num2;
}
一次导入,用逗号将默认的本地名称与大括号包裹的非默认值分隔开,请记住,在import语句中,默认值必须排在非默认值的前面(export无所谓)
import sum, {color} from './test2'
// import {default as sum , color} from './test2' // 如果需要重命名
console.log(sum(1,2));
console.log(color);
重新导出一个绑定
import { sum } from './test2'
export { sum }
// or
export {sum} from './test2'
// all
export * from './test2'
无绑定导入
某些模块可能不导出任何东西,相反,它们可能只修改全局作用域中的对象。
例如,向所有数组添加pushAll()方法
Array.prototype.pushAll = function(items){
if(!Array.isArray(items)){
throw new TypeError('参数必须是数组');
}
// 使用内建的push和展开运算符
return this.push(...items);
}
即使没有任何导入导出操作,它也是一个有效模块,因而可以使用简化的导入操作来执行模块代码,并且不导入任何的绑定:
import './test'
加载模块
在<script>标签中使用模块
加载模块文件与加载脚本之间的唯一区别是type的值是“module”
第二个script元素包含了直接嵌入在网页中的模块。变量result没有暴露到全局作用域,它只存在于模块中,因此不会被添加到window作为他的属性
此外,不支持模块的浏览器将自动忽略 <script type="module">
来提供良好的向后兼容性。
<!-- 加载一个JavaScript模块文件 -->
<script type="module" src="module.js"></script>
<!-- 内联引入一个模块 -->
<script type="module">
import {sum} from "./example.js"; let result = sum(1,2);
</script>
Web浏览器中的模块加载顺序
模块与脚本不同,他是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些文件必须被加载进该模块才能正确执行。为了支持该功能,<script type="module">
执行时自动应用defer
属性。
加载脚本文件时,defer是可选属性;加载模块时,它就是必须属性。一旦HTML解析器遇到具有src属性的<script type="module">
,模块文件便开始下载,直到文件被完全解析模块才会执行。模块按照它们出现在HTML文件中的顺序执行,也就是说,无论模块中包含的是内联代码还是指定src属性,第一个<script type="module">
总是在第二个之前执行
<!-- 先执行这个标签 -->
<script type="module" src="module.js"></script>
<!-- 再执行这个标签 -->
<script type="module">
import {sum} from "./example.js"; let result = sum(1,2);
</script>
<!-- 最后执行这个标签 -->
<script type="module" src="module2.js"></script>
</body>
这三个script元素按照它们被指定的顺序执行,所以模块module1保证会在内联模块之前执行。
每个模块都可以从一个或多个其他的模块导入,这会使问题复杂化。因此,首先解析模块以识别所有导入语句;然后,每个导入语句都触发一次获取过程(网络或者缓存),并且在所有导入资源都被加载和执行后才会执行当前模块。
用<script type="module">
显式引入和用import隐式导入的所有模块都是那徐加载并执行的。在这个实例中,完整的加载顺序如下:
- 下载并解析module1.js。
- 递归下载并解析module1中导入的资源。
- 解析内联模块。
- 递归下载并解析内联模块中导入的资源。
- 下载并解析module2.js。
- 递归下载并解析module2中导入的资源。
加载完成后,只有当文档完全被解析之后才会执行其他操作。文档解析完成后,会发生以下操作:
- 递归执行module1.js中导入的资源。
- 执行module1.js。
- 递归执行内联模块中导入的资源。
- 执行内联模块。
- 递归执行module2.js中导入的资源。
- 执行module2.js。
Web浏览器中的异步模块加载
script元素上的async属性,当其应用于脚本时,脚本文件将在文件完全下载并解析后执行。文档中async脚本的顺序不会影响脚本执行的顺序,脚本在下载完成后立即执行,而不必等待包含的文档完成解析
<!-- 无法保证这两个哪个先执行 -->
<script type="module" async src="module.js"></script>
<script type="module" async src="module2.js"></script>
网友评论