读书笔记:深入理解ES6 (十三)

原创
2019/10/22 14:51
阅读数 31

第十三章 用模块封装代码

  其他语言使用诸如包这样的概念来定义代码作用域,在ES6以前,Javascript用“共享一切”的方法加载代码,定义的一切都共享一个全局作用域,随着Web应用程序更加复杂,代码量的增加,这一做法会引起命名冲突、安全问题等。

  ES6的一个目标是解决作用域问题,也为了让程序显得有序,于是引入了模块。

 

第1节 什么是模块?

  1. 模块的定义

   模块是自动运行在严格模式下,并且没有办法退出运行的JavaScript代码。

  

  2. 模块的特性

    a) 在模块顶部创建的变量,仅在模块的顶级作用域中存在,不会自动被添加到全局共享作用域。

    b) 模块必须导出一些外部可以访问的元素,如变量、函数。

    c) 在模块的顶部,this 值是 undefined。

    d) 模块不支持 HTML 风格的代码注释。

 

  3. 模块和脚本的区别

    a)脚本是任何不是模块的JavaScript代码。

    b)脚本和模块代表了 JavaScript 代码加载和求值的一个重要变化。

 

第2节 导出的基本语法

  1. 可以用 export 关键字将一部分已发布的代码暴露给其他模块,即将 export 放在任何变量、函数或类声明的前面,以将它们从模块导出。

  2. 除了 export 关键字外,每一个声明与脚本中的一摸一样,除非用 default 关键字,否则不能用这个语法导出匿名函数类。

  3. 任何未显式导出的变量、函数和类都是模块私有的,无法从模块外部访问。

 

第3节 导入的基本语法

  1. 可以通过 import 关键字在另一个模块中访问从模块中导出的功能。

 

  2. import语句的两个部分是:要导入的标识符和标识符应当从哪个模块导入。举例:

1 import { identifier1, identifier2 } from "./example.js";

  

  3. import 后面的大括号表示从给定模块导入的绑定(binding)。

   当从模块中导入一个绑定时,它就好像使用 cons t定义的一样。结果是你无法定义另一个同名变量(包括导入另一个同名绑定),也无法在 import 语句前使用标识符或改变绑定的值。

 

  4. 可以导入单个绑定,也可以导入多个绑定,特殊情况下,可以将整个模块作为一个单一的对象导入,然后所有的导出都可以作为对象的属性使用。这样导入整个模块的格式被称作命名空间导入(namespace import)。

 

  5. export、import 的一个重要限制是,它们必须在其他语句和函数之外使用。举例:

复制代码
1 if (flag)
2 {
3     export flag; //语法错误
4 }
5 
6 //another example
7 function tryImport() {
8     import flag from "./example.js"; // 语法错误
9 }
复制代码

 

第4节 导出和导入时重命名

  有时候,在导出、导入的过程中,可能不希望使用它们的原始名称,这个时候,可以使用 as 关键字来实现这一需求。举例:

复制代码
 1 //导出的模块代码
 2 function sum (sum1, sum2)
 3 {
 4     return num1 + num2;
 5 } 
 6 
 7 export {sum as add}
 8 
 9 //导入的模块代码
10 import { add } from "./example.js"
11 
12 //也可以这样导入
13 import {add as sum } from "./example.js"
复制代码

 

第5节 模块的默认值

  1. 模块的默认值指的是通过default关键字指定的单个变量、函数或者类,只能为每一个模块设置一个默认的导出值,导出时多次使用default关键字是一个语法错误。举个导出默认值的例子:

1 export default function(num1, num2) {
2     return num1 + num2;
3 }

 

  2. 由于default是Javascript中的默认关键字,因此不能将其用于变量、函数或者类的名称,但是可以将其用作属性名称。

 

  3. 对于导出默认值和一个或多个非默认绑定的模块,可以用一条语句导入所有导出的绑定。这个时候,需要用逗号将默认的本地名称与大括号包裹的非默认值分开。记住,在import语句中,默认值必须排在非默认值前。

    详细代码见P.321-P.323

 

第6节 重新导出一个绑定

  1. 有的场景可能用到重新导出已经导入的值。例如:

1 import { sum } from "./example.js";
2 export { sum }

  

  2. 虽然上面的代码可以运行,但是只通过一条语句也可以完成同样的任务:

1 export { sum } from "./example.js"

  这种形式的 export 在指定的模块中查找sum声明,然后将其导出。

 

  3. 对于同样的值也可以用不同的名称导出:

1 export { sum as add } from "./example.js"

 

  4. 可以使用 “*” 来导出另一个模块中的所有值,导出一切即导出默认值及所有命名导出值,这可能会影响从模块导出的内容。例如:如果exmple.js有默认的导出值,则使用此语法时将无法定义一个新的默认导出。

 

第7节 无绑定导入

  1. 尽管模块中的顶层变量、函数和类不会自动地出现在全局作用域中,但这并不意味着模块无法访问全局作用域。内建对象(如Array和Object)的共享定义可以在模块中访问,对这些对象所做的更改将反映在其他模块中。例如:

复制代码
 1 //没有export或者import的代码
 2 Array.prototype.pushAll() = function(items)
 3 {
 4     //items必须是一个数组
 5     if (!Array.isArray(items)) 
 6     {
 7         throw new TypeError("参数必须是一个数组");
 8     }
 9 
10     //使用内建的push()和展开运算符
11     return this.push(...items);
12 }
复制代码

  

  2. 即使没有任何导出或者导入操作,上面的例子也是一个有效的模块。这段代码既可以用作模块,也可以用作脚本。

 

  3. 由于这种模块既不导出,也不导入,所以可以使用更加简洁的导入操作语法来执行模块代码,而且不需要导入任何绑定。例如:

1 import "./example.js";
2 
3 let colors = ["red", "green", "blue"];
4 let items = [];
5 
6 items.pushAll(colors);

  

  4. 无绑定导入最有可能被应用于创建polyfill和shim。

 

第8节 加载模块

  ES6定义了模块的语法,但并没有定义如何加载这些模块。这是规范复杂性的一个体现,应由不同的实现环境来决定(例如:Web浏览器和Node.js环境)

 

  1. 在Web浏览器中使用模块。

    有三种方法:

    1)在<script>元素中,通过src属性来加载JavaScript代码文件。

    2)将JavaScript代码内嵌到没有src属性的<script>元素中。

    3)通过Web worker或Service Worker加载JavaScript代码文件。

 

  2. Web浏览器中的模块加载顺序:

   其中,模块按照它们出现在HTML文件中的顺序执行。举例:

复制代码
 1 <!-- 先执行这个标签 -->
 2 <script type="module" src="module1.js"></script>
 3     
 4 <!-- 再执行这个标签 -->
 5 <script type="module" src="module2.js">
 6 import {sum} from "./example.js"
 7 
 8 let result = sum(1,2);
 9 </script>
10 
11 <!-- 最后执行这个标签 -->
12 <script type="module" src="module2.js"></script>
复制代码

    在这个例子中,首先解析模块以识别所有导入语句;然后,每个导入语句都触发一次获取过程,并且在所有导入资源都被加载和执行后,才会执行当前模块。完整的加载顺序如下:

    1. 下载并解析module1.js。

    2.  递归下载并解析module1.js中导入的资源。

    3. 解析内联模块。

    4. 递归下载并解析内联模块中导入的资源。

    5. 下载并解析module2.js。

    6. 递归下载并解析module2.js中导入的资源。

 

  加载完成后,只有当文档完全被解析之后才会执行其他操作。文档解析完成后,会发生以下操作:

    1. 递归执行module1.js中导入的资源。

    2. 执行module1.js。

    3. 递归执行内联模块中导入的资源。

    4. 执行内联模块。

    5. 递归执行module2.js中导入的资源。

    6. 执行module2.js。

 

  3. Web浏览器中的异步模块加载

    <script>元素上有async属性,文档中的 async脚本的顺序不会影响脚本执行的顺序,脚本在下载完成后立即执行,而不必等待包含的文档完成解析。

    async属性也可以用在模块上,与脚本类似的方式执行。唯一区别是,在模块执行前,模块中所有的导入资源都必须下载下来。

 

  4. 将模块作为Worker加载

    Worker,例如Web worker或Service worker,可以在网页上下文之外执行JavaScript代码。创建Worker步骤:创建实例 + 传入JavaScript文件的地址。例如:

1 //按照脚本的方式加载script.js
2 let worker = new Worker("script.js");

    

    为了支持加载模块,HTML标准的开发者向这些构造函数添加了第二个参数。第二个参数是一个对象,其type属性默认为“script”,可以将type设置为module来加载模块文件。例如:

1 //按照模块的方式加载module.js
2 let worker = new Worker("module.js", { type: "module" });

 

  5. 浏览器模块说明符解析

    1)模块说明符(module specifier)在前面的例子都是相对路径(如:字符串“./example.js”)。浏览器要求模块说明符具有以下几种格式之一:

      ·以 / 开头的解析为从根目录开始。

      ·以 ./ 开头的解析为从当前目录开始。

      ·以 ../ 开头的解析为从父目录开始。

      ·URL格式。

      例如,假设有一个模块文件位于 https://www.example.com/modules/module.js,其中包含以下代码:

复制代码
 1 // 从 https://www.example.com/modules/example.js 导入
 2 import { first } from "./example.js";
 3 
 4 // 从 https://www.example.com/example2.js 导入
 5 import { second } from "../example.js";
 6 
 7 // 从 https://www.example.com/example3.js 导入
 8 import { third } from "/example.js";
 9 
10 // 从 https://www.example.com/example4.js 导入
11 import { fourth } from "https://www.example.com/example4.js";
复制代码

 

    2)<script>标签和import之间引用模块,对模块说明符稍有不同,这个不同是有意为之。例如,

1 //无效的,没有以 /、 ./ 或 ../ 开头
2 import { first } from "example.js"
3 
4 //无效的,没有以 /、 ./ 或 ../ 开头
5 import {second} from "example/index.js"

 

 

(本节完)

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部