1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
This commit is contained in:
wizardforcel 2018-05-08 22:52:49 +08:00
parent 89b1e6a5e5
commit 7086d0fa98

136
10.md
View File

@ -1,9 +1,21 @@
# 十、模块
> 编写易于删除,而不是易于扩展的代码。
> 原文:[Modules](http://eloquentjavascript.net/10_modules.html)
>
> 译者:[飞龙](https://github.com/wizardforcel)
>
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
>
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
>
> 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
> 编写易于删除,而不是易于扩展的代码。
>
> Tef《Programming is Terrible》
![](img/10-0.jpg)
理想的程序拥有清晰的结构。 它的工作方式很容易解释,每个部分都起到明确的作用。
典型的真实程序会有机地增长。 新功能随着新需求的出现而增加。 构建和维护结构是额外的工作,只有在下一次有人参与该计划时,才会得到回报。 所以它易于忽视,并让程序的各个部分变得深深地纠缠在一起。
@ -22,13 +34,13 @@
为了以这种方式分离模块,每个模块需要它自己的私有作用域。
的 JavaScript 代码放入不同的文件,不能满足这些要求。 这些文件仍然共享相同的全局命名空间。 他们可以有意或无意干扰彼此的绑定。 依赖性结构仍不清楚。 我们将在本章后面看到,我们可以做得更好。
的 JavaScript 代码放入不同的文件,不能满足这些要求。 这些文件仍然共享相同的全局命名空间。 他们可以有意或无意干扰彼此的绑定。 依赖性结构仍不清楚。 我们将在本章后面看到,我们可以做得更好。
合适的模块结构可能难以为程序设计。 在你还在探索这个问题的阶段,尝试不同的事情来看看什么是可行的,你可能不想过多担心它,因为这可能让你分心。 一旦你有一些感觉可靠的东西,现在是后退一步并组织它的好时机。
## 包
从单独的片段中构建一个程序,并实际上能够独立运行这些片段的一个优点是,可能能够在不同的程序中应用相同的部分。
从单独的片段中构建一个程序,并实际上能够独立运行这些片段的一个优点是,可能能够在不同的程序中应用相同的部分。
但如何实现呢? 假设我想在另一个程序中使用第 9 章中的`parseINI`函数。 如果清楚该函数依赖什么(在这种情况下什么都没有),我可以将所有必要的代码复制到我的新项目中并使用它。 但是,如果我在代码中发现错误,我可能会在当时正在使用的任何程序中将其修复,并忘记在其他程序中修复它。
@ -40,7 +52,7 @@
以这种方式工作需要基础设施。 我们需要一个地方来存储和查找包,以及一个便利方式来安装和升级它们。 在 JavaScript 世界中,这个基础结构由 [NPM](https://npmjs.org) 提供。
NPM 是两个东西:可下载(和上传)包的在线服务,以及可帮助安装和管理它们的程序(与 Node.js 捆绑在一起)。
NPM 是两个东西:可下载(和上传)包的在线服务,以及可帮助安装和管理它们的程序(与 Node.js 捆绑在一起)。
在撰写本文时NPM 上有超过 50 万个不同的包。 其中很大一部分是垃圾,我应该提一下,但几乎所有有用的公开包都可以在那里找到。 例如,一个 INI 文件解析器,类似于我们在第 9 章中构建的那个,可以在包名称`ini`下找到。
@ -50,15 +62,15 @@ NPM 是两个东西:可下载(和上传)包的在线服务,以及可帮
软件的复制很便宜,所以一旦有人编写它,分发给其他人是一个高效的过程。但首先把它写出来是工作量,回应在代码中发现问题的人,或者想要提出新功能的人,是更大的工作量。
默认情况下,您拥有您编写的代码的版权,其他人只有经过您的许可才能使用它。但是因为有些人不错,而且由于发布好的软件可以使你在程序员中出名,所以许多包都会在许可证下发布,明确允许其他人使用它。
默认情况下,你拥有你编写的代码的版权,其他人只有经过你的许可才能使用它。但是因为有些人不错,而且由于发布好的软件可以使你在程序员中出名,所以许多包都会在许可证下发布,明确允许其他人使用它。
NPM 上的大多数代码都以这种方式授权。某些许可证要求还要在相同许可证下发布基于那个包构建的代码。其他要求不高,只是要求在分发代码时保留许可证。 JavaScript 社区主要使用后一种许可证。使用其他人的包时,请确保留意了他们的许可证。
NPM 上的大多数代码都以这种方式授权。某些许可证要求还要在相同许可证下发布基于那个包构建的代码。其他要求不高,只是要求在分发代码时保留许可证。 JavaScript 社区主要使用后一种许可证。使用其他人的包时,请确保留意了他们的许可证。
## 即兴的模块
2015 年之前JavaScript 语言没有内置的模块系统。 然而,尽管人们已经用 JavaScript 构建了十多年的大型系统,他们需要模块。
所以他们在语言之上设计了自己的模块系统。 可以使用 JavaScript 函数创建局部作用域,并使用对象来表示模块接口。
所以他们在语言之上设计了自己的模块系统。 可以使用 JavaScript 函数创建局部作用域,并使用对象来表示模块接口。
这是一个模块,用于日期名称和数字之间的转换(由`Date``getDay`方法返回)。 它的接口由`weekDay.name``weekDay.number`组成,它将局部绑定名称隐藏在立即调用的函数表达式的作用域内。
@ -113,11 +125,11 @@ console.log(plusOne(4));
用于连接 JavaScript 模块的最广泛的方法称为 CommonJS 模块。 Node.js 使用它,并且是 NPM 上大多数包使用的系统。
CommonJS 模块的主要概念是称为`require`的函数。 当使用依赖项的模块名称调用这个函数时,它会确保该模块已加载并返回其接口。
CommonJS 模块的主要概念是称为`require`的函数。 当使用依赖项的模块名称调用这个函数时,它会确保该模块已加载并返回其接口。
由于加载器将模块代码封装在一个函数中,模块自动得到它们自己的局部作用域。 他们所要做的就是,调用`require`来访问它们的依赖关系,并将它们的接口放在绑定到`exports`的对象中。
此示例模块提供了日期格式化功能。 它使用 NPM的两个软件包,`ordinal`用于将数字转换为字符串,如`"1st"``"2nd"`,以及`date-names`用于获取星期和月份的英文名称。 它导出函数`formatDate`,它接受一个`Date`对象和一个模板字符串。
此示例模块提供了日期格式化功能。 它使用 NPM的两个包`ordinal`用于将数字转换为字符串,如`"1st"``"2nd"`,以及`date-names`用于获取星期和月份的英文名称。 它导出函数`formatDate`,它接受一个`Date`对象和一个模板字符串。
模板字符串可包含指明格式的代码,如`YYYY`用于全年,`Do`用于每月的序数日。 你可以给它一个像`"MMMM Do YYYY"`这样的字符串,来获得像`"November 22nd 2017"`这样的输出。
@ -193,7 +205,7 @@ CommonJS 模块很好用,并且与 NPM 一起,使 JavaScript 社区开始大
但他们仍然是个简单粗暴的黑魔法。 例如,表示法有点笨拙 - 添加到`exports`的内容在局部作用域中不可用。 而且因为`require`是一个正常的函数调用,接受任何类型的参数,而不仅仅是字符串字面值,所以在不运行代码就很难确定模块的依赖关系。
这就是 2015 年的 JavaScript 标准引入了自己的不同模块系统的原因。 它通常被称为 ES 模块,其中 ES 代表 ECMAScript。 依赖和接口的主要概念保持不变,但细节不同。 首先,表示法现在已整合到该语言中。 不用调用函数来访问依赖关系,而是使用特殊的`import`关键字。
这就是 2015 年的 JavaScript 标准引入了自己的不同模块系统的原因。 它通常被称为 ES 模块,其中 ES 代表 ECMAScript。 依赖和接口的主要概念保持不变,但细节不同。 首先,表示法现在已整合到该语言中。 不用调用函数来访问依赖关系,而是使用特殊的`import`关键字。
```js
import ordinal from "ordinal";
@ -206,7 +218,7 @@ export function formatDate(date, format) { /* ... */ }
ES 模块的接口不是单个值,而是一组命名绑定。 前面的模块将`formatDate`绑定到一个函数。 从另一个模块导入时,导入绑定而不是值,这意味着导出模块可以随时更改绑定的值,导入它的模块将看到其新值。
当有一个名为`default`的绑定时,它将被视为模块的主要导出值。 如果在示例中导入了一个类似于`ordinal`的模块,而没有绑定名称周围的大括号,则会获得其默认绑定。 除了默认绑定之外,这些模块仍然可以以不同名称导出其他绑定。
当有一个名为`default`的绑定时,它将被视为模块的主要导出值。 如果在示例中导入了一个类似于`ordinal`的模块,而没有绑定名称周围的大括号,则会获得其默认绑定。 除了默认绑定之外,这些模块仍然可以以不同名称导出其他绑定。
为了创建默认导出,可以在表达式,函数声明或类声明之前编写`export default`
@ -239,5 +251,105 @@ console.log(dayNames.length);
我们可以再深入一点。 除了文件的数量之外,文件的大小也决定了它们可以通过网络传输的速度。 因此JavaScript 社区发明了压缩器。 通过自动删除注释和空白,重命名绑定以及用占用更少空间的等效代码替换代码段,这些工具使 JavaScript 程序变得更小。
因此,您在 NPM 软件包中找到的代码,或运行在网页上的代码,经历了多个转换阶段 - 从现代 JavaScript 转换为历史 JavaScript从 ES 模块格式转换为 CommonJS打包并压缩。 我们不会在本书中详细介绍这些工具,因为它们往往很无聊,并且变化很快。 请注意,运行的 JavaScript 代码通常不是编写的代码。
因此,你在 NPM 包中找到的代码,或运行在网页上的代码,经历了多个转换阶段 - 从现代 JavaScript 转换为历史 JavaScript从 ES 模块格式转换为 CommonJS打包并压缩。 我们不会在本书中详细介绍这些工具,因为它们往往很无聊,并且变化很快。 请注意,运行的 JavaScript 代码通常不是编写的代码。
## 模块设计
使程序结构化是编程的一个微妙的方面。 任何有价值的功能都可以用各种方式建模。
良好的程序设计是主观的 - 涉及到权衡和品味问题。 了解结构良好的设计的价值的最好方法,是阅读或处理大量程序,并注意哪些是有效的,哪些不是。 不要认为一个痛苦的混乱就是“它本来的方式”。 通过多加思考,你可以改善几乎所有事物的结构。
模块设计的一个方面是易用性。 如果你正在设计一些旨在由多人使用,或者甚至是你自己的东西,在三个月之内,当你记不住你所做的细节时,如果你的接口简单且可预测,这会有所帮助。
这可能意味着遵循现有的惯例。 `ini`包是一个很好的例子。 此模块模仿标准 JSON 对象,通过提供`parse``stringify`(用于编写 INI 文件)函数,就像 JSON 一样,在字符串和普通对象之间进行转换。 所以接口很小且很熟悉,在你使用过一次后,你可能会记得如何使用它。
即使没有能模仿的标准函数或广泛使用的包,你也可以通过使用简单的数据结构,并执行单一的重点事项,来保持模块的可预测性。 例如NPM 上的许多 INI 文件解析模块,提供了直接从硬盘读取文件并解析它的功能。 这使得在浏览器中不可能使用这些模块,因为我们没有文件系统的直接访问权,并且增加了复杂性,通过组合模块与某些文件读取功能,可以更好地解决它。
这指向了模块设计的另一个有用的方面 - 一些代码可以轻易与其他代码组合。比起执行带有副作用的复杂操作的更大的模块,计算值的核心模块适用于范围更广的程序。坚持从磁盘读取文件的 INI 文件读取器, 在文件内容来自其他来源的场景中是无用的。
与之相关,有状态的对象有时甚至是有用的,但是如果某件事可以用一个函数完成,就用一个函数。 NPM 上的几个 INI 文件读取器提供了一种接口风格,需要你先创建一个对象,然后将该文件加载到对象中,最后使用特定方法来获取结果。这种类型的东西在面向对象的传统中很常见,而且很糟糕。你不能调用单个函数来完成,你必须执行仪式,在各种状态中移动对象。而且由于数据现在封装在一个特定的对象类型中,与它交互的所有代码都必须知道该类型,从而产生不必要的相互依赖关系。
通常,定义新的数据结构是不可避免的 - 只有少数非常基本的数据结构由语言标准提供,并且许多类型的数据一定比数组或映射更复杂。 但是当数组足够时,使用数组。
一个稍微复杂的数据结构的示例是第 7 章的图。JavaScript 中没有一种明显的表示图的方式。 在那一章中,我们使用了一个对象,其属性保存了字符串数组 - 可以从某个节点到达的其他节点。
NPM 上有几种不同的寻路包,但他们都没有使用这种图的格式。 它们通常允许图形边缘带有权重,它是与其相关的成本或距离,这在我们的表示中是不可能的。
例如,存在`dijkstrajs`包。 一种著名的寻路方法,与我们的`findRoute`函数非常相似它被称为迪科斯特拉Dijkstra算法以首先编写它的艾兹格尔·迪科斯特拉Edsger Dijkstra命名。 `js`后缀通常会添加到包名称中,以表明它们用 JavaScript 编写。 这个`dijkstrajs`包使用类似于我们的图的格式,但是它不使用数组,而是使用对象,它的属性值是数字 - 边的权重。
所以如果我们想要使用这个包,我们必须确保我们的图以它期望的格式存储。 所有边的权重都相同,因为我们的简化模型将每条道路视为具有相同的成本(一个回合)。
```js
const {find_path} = require("dijkstrajs");
let graph = {};
for (let node of Object.keys(roadGraph)) {
let edges = graph[node] = {};
for (let dest of roadGraph[node]) {
edges[dest] = 1;
}
}
console.log(find_path(graph, "Post Office", "Cabin"));
// → ["Post Office", "Alice's House", "Cabin"]
```
这可能是组合的障碍 - 当各种包使用不同的数据结构来描述类似的事情时,将它们组合起来很困难。 因此,如果你想要设计可组合性,请查找其他人使用的数据结构,并在可能的情况下遵循他们的示例。
## 总结
通过将代码分离成具有清晰接口和依赖关系的块,模块是更大的程序结构。 接口是模块中可以从其他模块看到的部分,依赖关系是它使用的其他模块。
由于 JavaScript 历史上并没有提供模块系统,因此 CommonJS 系统建立在它之上。 然后在某个时候,它确实有了一个内置系统,它现在与 CommonJS 系统不兼容。
包是可以自行分发的一段代码。 NPM 是 JavaScript 包的仓库。 你可以从上面下载各种有用的(和无用的)包。
## 练习
### 模块机器人
这些是第 7 章的项目所创建的约束:
```
roads
buildGraph
roadGraph
VillageState
runRobot
randomPick
randomRobot
mailRoute
routeRobot
findRoute
goalOrientedRobot
```
如果你要将该项目编写为模块化程序,你会创建哪些模块? 哪个模块依赖于哪个模块,以及它们的接口是什么样的?
哪些片段可能在 NPM 上找到? 你愿意使用 NPM 包还是自己编写?
### `roads`模块
根据第 7 章中的示例编写 CommonJS 模块,该模块包含道路数组,并将表示它们的图数据结构导出为`roadGraph`。 它应该依赖于一个模块`./graph`,它导出一个函数`buildGraph`,用于构建图。 该函数接受包含两个元素的数组(道路的起点和终点)。
```js
// Add dependencies and exports
const roads = [
"Alice's House-Bob's House", "Alice's House-Cabin",
"Alice's House-Post Office", "Bob's House-Town Hall",
"Daria's House-Ernie's House", "Daria's House-Town Hall",
"Ernie's House-Grete's House", "Grete's House-Farm",
"Grete's House-Shop", "Marketplace-Farm",
"Marketplace-Post Office", "Marketplace-Shop",
"Marketplace-Town Hall", "Shop-Town Hall"
];
```
### 循环依赖
循环依赖是一种情况,其中模块 A 依赖于 B并且 B 也直接或间接依赖于 A。许多模块系统完全禁止这种情况因为无论你选择何种顺序来加载此类模块都无法确保每个模块的依赖关系在它运行之前加载。
CommonJS 模块允许有限形式的循环依赖。 只要这些模块不会替换它们的默认`exports`对象,并且在完成加载之后才能访问对方的接口,循环依赖就没有问题。
本章前面给出的`require`函数支持这种类型的循环依赖。 你能看到它如何处理循环吗? 当一个循环中的某个模块替代其默认`exports`对象时,会出现什么问题?