├── .gitignore
├── 0.md
├── 1.md
├── 10.md
├── 11.md
├── 12.md
├── 13.md
├── 14.md
├── 15.md
├── 16.md
├── 17.md
├── 18.md
├── 19.md
├── 2.md
├── 20.md
├── 21.md
├── 3.md
├── 4.md
├── 5.md
├── 6.md
├── 7.md
├── 8.md
├── 9.md
├── README.md
├── SUMMARY.md
├── cover.jpg
├── diff-en
├── 2ech0-3ech0.diff
├── 2ech1-3ech1.diff
├── 2ech11-3ech12.diff
├── 2ech12-3ech13.diff
├── 2ech13-3ech14.diff
├── 2ech14-3ech15.diff
├── 2ech15-3ech16.diff
├── 2ech16-3ech17.diff
├── 2ech17-3ech18a.diff
├── 2ech18-3ech18b.diff
├── 2ech2-3ech2.diff
├── 2ech20-3ech20.diff
├── 2ech21-3ech21.diff
├── 2ech3-3ech3.diff
├── 2ech4-3ech4.diff
├── 2ech5-3ech5.diff
├── 2ech6-3ech6.diff
├── 2ech8-3ech8.diff
└── 2ech9-3ech9.diff
├── img
├── 0-0.jpg
├── 1-0.jpg
├── 10-0.jpg
├── 11-1.svg
├── 11-2.png
├── 12-0.jpg
├── 12-1.svg
├── 13-0.jpg
├── 14-0.jpg
├── 14-1.svg
├── 14-2.svg
├── 14-3.svg
├── 14-4.svg
├── 15-0.jpg
├── 16-0.jpg
├── 16-1.png
├── 16-2.svg
├── 17-0.jpg
├── 17-1.png
├── 17-2.svg
├── 17-3.svg
├── 17-4.png
├── 17-5.png
├── 18-0.jpg
├── 19-0.jpg
├── 19-1.png
├── 19-2.svg
├── 19-3.svg
├── 2-0.jpg
├── 2-1.png
├── 2-2.svg
├── 2-3.svg
├── 2-4.svg
├── 2-5.svg
├── 20-0.jpg
├── 21-0.jpg
├── 21-1.png
├── 3-0.jpg
├── 4-0.jpg
├── 4-1.svg
├── 4-2.jpg
├── 4-3.svg
├── 5-0.jpg
├── 5-1.png
├── 6-0.jpg
├── 6-1.svg
├── 7-0.jpg
├── 7-1.png
├── 8-0.jpg
├── 9-0.jpg
├── 9-1.svg
├── 9-2.svg
├── 9-3.svg
└── qr_alipay.png
└── styles
└── ebook.css
/.gitignore:
--------------------------------------------------------------------------------
1 | _book
2 | Thumbs.db
3 |
--------------------------------------------------------------------------------
/0.md:
--------------------------------------------------------------------------------
1 | # 零、前言
2 |
3 | > 原文:[Introduction](https://eloquentjavascript.net/00_intro.html)
4 | >
5 | > 译者:[飞龙](https://github.com/wizardforcel)
6 | >
7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
8 | >
9 | > 自豪地采用[谷歌翻译](https://translate.google.cn/)
10 | >
11 | > 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
12 |
13 | > We think we are creating the system for our own purposes. We believe we are making it in our own image... But the computer is not really like us. It is a projection of a very slim part of ourselves: that portion devoted to logic, order, rule, and clarity.
14 |
15 | > Ellen Ullman,《Close to the Machine: Technophilia and its Discontents》
16 |
17 | 
18 |
19 | 这是一本关于指导电脑的书。时至今日,计算机就像螺丝刀一样随处可见,但相比于螺丝刀而言,计算机更复杂一些,并且,让他们做你想让他们做的事情,并不总是那么容易。
20 |
21 | 如果你让计算机执行的任务是常见的,易于理解的任务,例如向你显示你的电子邮件,或像计算器一样工作,则可以打开相应的应用并开始工作。 但对于独特的或开放式的任务,应用可能不存在。
22 |
23 |
24 | 这就是编程可能出现的地方。编程是构建一个程序的行为 - 它是一组精确的指令,告诉计算机做什么。 由于计算机是愚蠢的,迂腐的野兽,编程从根本上是乏味和令人沮丧的。
25 |
26 | 幸运的是,如果你可以克服这个事实,并且甚至可以享受愚蠢机器可以处理的严谨思维,那么编程可以是非常有益的。 它可以让你在几秒钟内完成手动操作。 这是一种方法,让你的电脑工具去做它以前做不到的事情。 它也提供了抽象思维的优秀练习。
27 |
28 | 大多数编程都是用编程语言完成的。 编程语言是一种人工构建的语言,用于指导计算机。 有趣的是,我们发现与电脑沟通的最有效的方式,与我们彼此沟通的方式相差太大。 与人类语言一样,计算机语言可以以新的方式组合词语和词组,从而可以表达新的概念。
29 |
30 | 在某种程度上,基于语言的界面,例如 80 年代和 90 年代的 BASIC 和 DOS 提示符,是与计算机交互的主要方法。 这些已经在很大程度上被视觉界面取代,这些视觉界面更容易学习,但提供更少的自由。 计算机语言仍然存在,如果你知道在哪里看到。 每种现代 Web 浏览器都内置了一种这样的语言,即 JavaScript,因此几乎可以在所有设备上使用。
31 |
32 | 本书将试图让你足够了解这门语言,从而完成有用和有趣的东西。
33 |
34 | ## 关于程序设计
35 |
36 | 除了讲解 JavaScript 之外,本书也会介绍一些程序设计的基本原则。程序设计还是比较复杂的。编程的基本规则简单清晰,但在这些基本规则之上构建的程序却容易变得复杂,导致程序产生了自己的规则和复杂性。即便程序是按照你自己的思路去构建的,你也有可能迷失在代码之间。
37 |
38 | 在阅读本书时,你有可能会觉得书中的概念难以理解。如果你刚刚开始学习编程,那么你估计还有不少东西需要掌握呢。如果你想将所学知识融会贯通,那么就需要去多参考和学习一些资料。
39 |
40 | 是否付出必要的努力完全取决于你自己。当你阅读本书的时候发现任何难点,千万不要轻易就对自己的能力下结论。只要能坚持下去,你就是好样的。稍做休息,复习一下所学的知识点,始终确保自己阅读并理解了示例程序和相关的练习。学习是一项艰巨的任务,但你掌握的所有知识都属于你自己,而且今后的学习道路会愈加轻松。
41 |
42 | > 当行动无利可图时,就收集信息;当信息无利可图时,就休息。
43 |
44 | > Ursula K. Le Guin,《The Left Hand of Darkness》
45 |
46 | 一个程序有很多含义:它是开发人员编写的一段文本、计算机执行的一段指令集合、计算机内存当中的数据以及控制内存中数据的操作集合。我们通常很难将程序与我们日常生活中熟悉的事物进行对比。有一种表面上比较恰当的比喻,即将程序视作包含许多组件的机器,为了让机器正常工作,这些组件通过内部通信来实现整个机器的正常运转。
47 |
48 | 计算机是一台物理机器,充当这些非物质机器的载体。计算机本身并不能实现多么复杂的功能,但计算机之所以有用是因为它们的运算速度非常快。而程序的作用就是将这些看似简单的动作组合起来,然后实现复杂的功能。
49 |
50 | 程序是思想的结晶。编写程序不需要什么物质投入,它很轻量级,通过我们的双手创造。
51 |
52 | 但如果不稍加注意,程序的体积和复杂度就会失去控制,甚至代码的编写者也会感到迷惑。在可控的范围内编写程序是编程过程中首要解决的问题。当程序运行时,一切都是那么美好。编程的精粹就在于如何更好地控制复杂度。质量高的程序的复杂度都不会太高。
53 |
54 | 很多开发人员认为,控制程序复杂度的最好方法就是避免使用不熟悉的技术。他们制定了严格的规则(“最佳实践”),并小心翼翼地呆在他们安全区内。
55 |
56 | 这不仅无聊,而且也是无效的。新问题往往需要新的解决方案。编程领域还很年轻,仍然在迅速发展,并且多样到足以为各种不同的方法留出空间。在程序设计中有许多可怕的错误,你应该继续犯错,以便你能理解它们。好的程序看起来是什么样的感觉,是在实践中发展的,而不是从一系列规则中学到的。
57 |
58 | ## 为什么编程语言重要
59 |
60 | 在计算技术发展伊始,并没有编程语言这个概念。程序看起来就像这样:
61 |
62 | ```
63 | 00110001 00000000 00000000
64 | 00110001 00000001 00000001
65 | 00110011 00000001 00000010
66 | 01010001 00001011 00000010
67 | 00100010 00000010 00001000
68 | 01000011 00000001 00000000
69 | 01000001 00000001 00000001
70 | 00010000 00000010 00000000
71 | 01100010 00000000 00000000
72 | ```
73 |
74 | 该程序计算数字 1~10 之和,并打印出结果:`1+2+...+10=55`。该程序可以运行在一个简单的机器上。在早期计算机上编程时,我们需要在正确的位置设置大量开关阵列,或在纸带上穿孔并将纸带输入计算机中。你可以想象这个过程是多么冗长乏味且易于出错。即便是编写非常简单的程序,也需要有经验的人耗费很大精力才能完成。编写复杂的程序则更是难上加难。
75 |
76 | 当然了,手动输入这些晦涩难懂的位序列(1 和 0)来编写程序的确能让程序员感到很有成就感,而且能给你的职业带来极大的满足感。
77 |
78 | 在上面的程序中,每行都包含一条指令。我们可以用中文来描述这些指令:
79 |
80 | 1. 将数字 0 存储在内存地址中的位置 0。
81 |
82 | 2. 将数字 1 存储在内存地址的位置 1。
83 |
84 | 3. 将内存地址的位置 1 中的值存储在内存地址的位置 2。
85 |
86 | 4. 将内存地址的位置 2 中的值减去数字 11。
87 |
88 | 5. 如果内存地址的位置 2 中的值是 0,则跳转到指令 9。
89 |
90 | 6. 将内存地址的位置 1 中的值加到内存地址的位置 0。
91 |
92 | 7. 将内存地址的位置 1 中的值加上数字 1。
93 |
94 | 8. 跳转到指令 3。
95 |
96 | 9. 输出内存地址的位置 0 中的值。
97 |
98 | 虽说这已经比一大堆位序列要好读了许多,但仍然不清晰。使用名称而不是数字用于指令和存储位置有所帮助:
99 |
100 | ```
101 | Set “total” to 0.
102 | Set “count” to 1.
103 | [loop]
104 | Set “compare” to “count”.
105 | Subtract 11 from “compare”.
106 | If “compare” is zero, continue at [end].
107 | Add “count” to “total”.
108 | Add 1 to “count”.
109 | Continue at [loop].
110 | [end]
111 | Output “total”.
112 | ```
113 |
114 | 现在你能看出该程序是如何工作的吗?前两行代码初始化两个内存位置的值:`total`用于保存累加计算结果,而`count`则用于记录当前数字。你可能觉得`compare`的那行代码看起来有些奇怪。程序想根据`count`是否等于 11 来决定是否应该停止运行。因为我们的机器相当原始,所以只能测试一个数字是否为 0,并根据它做出决策。因此程序用名为`compare`的内存位置存放`count–11`的值,并根据该值是否为 0 决定是否跳转。接下来两行将`count`的值累加到结果上,并将`count`加 1,直到`count`等于`11`为止。
115 |
116 | 下面使用 JavaScript 重新编写了上面的程序:
117 |
118 | ```js
119 | let total = 0, count = 1;
120 | while (count <= 10) {
121 | total += count;
122 | count += 1;
123 | }
124 | console.log(total);
125 | // → 55
126 | ```
127 |
128 | 这个版本的程序得到了一些改进。更为重要的是,我们再也不需要指定程序如何来回跳转了,而是由`while`结构负责完成这个任务。只要我们给予的条件成立,`while`语句就会不停地执行其下方的语句块(包裹在大括号中)。而我们给予的条件是`count<=10`,意思是“`count`小于等于 10”。我们再也不需要创建临时的值并将其与 0 比较,那样的代码十分烦琐。编程语言的一项职责就是,能够帮助我们处理这些烦琐无趣的逻辑。
129 |
130 | 在程序的结尾,也就是`while`语句结束后,我们使用`console.log`操作来输出结果。
131 |
132 | 最后,我们恰好有`range`和`sum`这类方便的操作。下面代码中的`range`函数用于创建数字集合,`sum`函数用于计算数字集合之和:
133 |
134 | ```js
135 | console.log(sum(range(1, 10)));
136 | // → 55
137 | ```
138 |
139 | 我们可以从这里了解到,同一个程序的长度可长可短,可读性可高可低。第一个版本的程序晦涩难懂,而最后一个版本的程序则接近于人类语言的表达方式:将 1~10 范围内的数字之和记录下来(我们会在后面的章节中详细介绍如何编写`sum`和`range`这样的函数)。
140 |
141 | 优秀的编程语言可以为开发人员提供更高层次的抽象,使用类似于人类语言的方式来与计算机进行交互。它有助于省略细节,提供便捷的积木(比如`while`和`console.log`),允许你定义自己的积木(比如`sum`和`range`函数),并使这些积木易于编写。。
142 |
143 | ## 什么是 JavaScript
144 |
145 | JavaScript 诞生于 1995 年。起初,Netscape Navigator 浏览器将其运用在网页上添加程序。自此以后,各类主流图形网页浏览器均采用了 JavaScript。JavaScript 使得现代网页应用程序成为可能 —— 使用 JavaScript 可以直接与用户交互,从而避免每一个动作都需要重新载入页面。但有许多传统网站也会使用 JavaScript 来提供实时交互以及更加智能的表单功能。
146 |
147 | JavaScript 其实和名为Java的程序设计语言没有任何关系。起了这么一个相似的名字完全是市场考虑使然,这并非是一个明智的决定。当 JavaScript 出现时,Java 语言已在市场上得到大力推广且拥有了极高人气,因此某些人觉得依附于 Java 的成功是个不错的主意。而我们现在已经无法摆脱这个名字了。
148 |
149 | 在 JavaScript 被广泛采用之后,ECMA 国际制订了一份标准文档来描述 JavaScript 的工作行为,以便所有声称支持 JavaScript 的软件都使用同一种语言。标准化完成后,该标准被称为 ECMAScript 标准。实际上,术语 ECMAScript 和 JavaScript 可以交换使用。它们不过是同一种语言的两个名字而已。
150 |
151 | 许多人会说 JavaScript 语言的坏话。这其中有很多这样的言论都是正确的。当被要求第一次使用 JavaScript 编写代码时,我当时就觉得这门语言难以驾驭。JavaScript 接受我输入的任何代码,但是又使用和我的想法完全不同的方式来解释代码。由于我没有任何线索知道我之前做了什么,因此我需要做出更多工作,但这也就存在一个实际问题:我们可以自由使用 JavaScript,而这种自由却几乎没有限度。这种设计其实是希望初学者更容易使用 JavaScript 编写程序。但实际上,系统不会指出我们错在何处,因此从程序中找出问题变得更加棘手。
152 |
153 | 但这种自由性也有其优势,许多技术在更为严格的语言中不可能实现,而在 JavaScript 中则留下了实现的余地,正如你看到的那样(比如第十章),有些优势可以弥补 JavaScript 的一些缺点。在正确地学习 JavaScript 并使用它工作了一段时间后,我真正喜欢上了 JavaScript。
154 |
155 | JavaScript 版本众多。大约在 2000~2010 年间,这正是 JavaScript 飞速发展的时期,浏览器支持最多的是 ECMAScript 3。在此期间,ECMA 着手制定 ECMAScript 4,这是一个雄心勃勃的版本,ECMA 计划在这个版本中加入许多彻底的改进与扩展。但由于 ECMAScript 3 被广泛使用,这种过于激进的修改必然会遭遇重重阻碍,最后 ECMA 不得不于 2008 年放弃了版本 4 的制定。这就产生了不那么雄心勃勃的版本 5,这只是一些没有争议的改进,出现在 2009 年。 然后版本 6 在 2015 年诞生,这是一个重大的更新,其中包括计划用于版本 4 的一些想法。从那以后,每年都会有新的更新。
156 |
157 | 语言不断发展的事实意味着,浏览器必须不断跟上,如果你使用的是较老的浏览器,它可能不支持每个特性。 语言设计师会注意,不要做任何可能破坏现有程序的改变,所以新的浏览器仍然可以运行旧的程序。 在本书中,我使用的是 2017 版的 JavaScript。
158 |
159 | Web 浏览器并不是唯一一个可以运行 JavaScript 的平台。有些数据库,比如 MongoDB 和 CouchDB,也使用 JavaScript 作为脚本语言和查询语言。一些桌面和服务器开发的平台,特别是 Node.js 项目(第二十章介绍),为浏览器以外的 JavaScript 编程提供了一个环境。
160 |
161 | ## 代码及相关工作
162 |
163 | 代码是程序的文本内容。本书多数章节都介绍了大量代码。我相信阅读代码和编写代码是学习编程不可或缺的部分。尝试不要仅仅看一眼示例,而应该认真阅读并理解每个示例。刚开始使用这种方式可能会速度较慢并为代码所困惑,但我坚信你很快就可以熟能生巧。对待习题的方法也应该一样。除非你确实已经编写代码解决了问题,否则不要假设你已经理解了问题。
164 |
165 | 建议读者应尝试在实际的 JavaScript 解释器中执行习题代码。这样一来,你就可以马上获知代码工作情况的反馈,而且我希望读者去做更多的试验,而不仅仅局限于习题的要求。
166 |
167 | 可以在 中查阅本书的在线版本,并运行和实验本书中的代码。也可以在在线版本中点击任何代码示例来编辑、运行并查看其产生的输出。在做习题时,你可以访问 ,该网址会提供每个习题的初始代码,让你专心于解答习题。
168 |
169 | 如果想要在本书提供的沙箱以外执行本书代码,需要稍加注意。许多的示例是独立的,而且可以在任何 JavaScript 环境下运行。但后续章节的代码大多数都是为特定环境(浏览器或者 Node.js)编写的,而且只能在这些特定环境下执行代码。此外,许多章节定义了更大的程序,这些章节中出现的代码片段会互相依赖或是依赖于一些外部文件。本书网站的沙箱提供了 zip 压缩文件的链接,该文件包含了所有运行特定章节代码所需的脚本和数据文件。
170 |
171 | ## 本书概览
172 |
173 | 本书包括三个部分。前十二章讨论 JavaScript 语言本身的一些特性。接下来的 8 章讨论网页浏览器和 JavaScript 在网页编程中的实践。最后两章专门讲解另一个使用 JavaScript 编程的环境 —— Node.js。
174 |
175 | 纵观本书,共有 5 个项目实战章,用于讲解规模较大的示例程序,你可以通过这些章来仔细品味真实的编程过程。根据项目出现次序,我们会陆续构建递送机器人(7)、程序设计语言(12)、平台游戏(16)、像素绘图程序(19)和一个动态网站(21)。
176 |
177 | 本书介绍编程语言时,首先使用4章来介绍 JavaScript 语言的基本结构,包括第二章控制结构(比如在本前言中看到的`while`单词)、第三章函数(编写你自己的积木)和第四章数据结构。此后你就可以编写简单的程序了。接下来,第五章和第六章介绍函数和对象的运用技术,以编写更加抽象的代码并以此来控制复杂度。
178 |
179 | 介绍完第一个项目实战(7)之后,将会继续讲解语言部分,例如第八章错误处理和 bug 修复、第九章正则表达式(处理文本数据的重要工具)、第十章模块化(解决复杂度的问题)以及第十一章异步编程(处理需要时间的事件)。第二个项目实战章节(12)则是对本书第一部分的总结。
180 |
181 | 第二部分(第十三章到第十九章),阐述了浏览器 JavaScript 中的一些工具。你将会学到在屏幕上显示某些元素的方法(第十四章与第十七章),响应用户输入的方法(第十五章)和通过网络通信的方法(第十八章)。这部分又有两个项目实战章节。
182 |
183 | 此后,第二十章阐述 Node.js,而第二十一章使用该工具构建一个简单的网页系统。
184 |
185 | ## 本书版式约定
186 |
187 | 本书中存在大量代码,程序(包括你迄今为止看到的一些示例)代码的字体如下所示:
188 |
189 | ```js
190 | function factorial(n) {
191 | if (n == 0) {
192 | return 1;
193 | } else {
194 | return factorial(n - 1) * n;
195 | }
196 | }
197 | ```
198 |
199 | 为了展示程序产生的输出,本书常在代码后编写代码期望输出,输出结果前会加上两个反斜杠和一个箭头。
200 |
201 | ```js
202 | console.log(factorial(8));
203 | // → 40320
204 | ```
205 |
206 | 祝好运!
207 |
--------------------------------------------------------------------------------
/1.md:
--------------------------------------------------------------------------------
1 | ## 一、值,类型和运算符
2 |
3 | > 原文:[Values, Types, and Operators](http://eloquentjavascript.net/01_values.html)
4 | >
5 | > 译者:[飞龙](https://github.com/wizardforcel)
6 | >
7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
8 | >
9 | > 自豪地采用[谷歌翻译](https://translate.google.cn/)
10 | >
11 | > 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
12 |
13 | > 在机器的表面之下,程序在运转。 它不费力就可以扩大和缩小。 在和谐的关系中,电子散开并重新聚合。 监视器上的表格只是水面上的涟漪。 本质隐藏在下面。
14 | >
15 | > Master Yuan-Ma,《The Book of Programming》
16 |
17 | 
18 |
19 | 计算机世界里只有数据。 你可以读取数据,修改数据,创建新数据 - 但不能提及不是数据的东西。 所有这些数据都以位的长序列存储,因此基本相似。
20 |
21 | 位是任何类型的二值的东西,通常描述为零和一。 在计算机内部,他们有一些形式,例如高电荷或低电荷,强信号或弱信号,或 CD 表面上的亮斑点或暗斑点。 任何一段离散信息都可以简化为零和一的序列,从而以位表示。
22 |
23 | 例如,我们可以用位来表示数字 13。 它的原理与十进制数字相同,但不是 10 个不同的数字,而只有 2 个,每个数字的权重从右到左增加 2 倍。 以下是组成数字 13 的位,下方显示数字的权重:
24 |
25 | ```
26 | 0 0 0 0 1 1 0 1
27 | 128 64 32 16 8 4 2 1
28 | ```
29 |
30 | 因此,这就是二进制数`00001101`,或者`8+4+1`,即 13。
31 |
32 | ## 值
33 |
34 | 想象一下位之海 - 一片它们的海洋。 典型的现代计算机的易失性数据存储器(工作存储器)中,有超过 300 亿位。非易失性存储(硬盘或等价物)往往还有几个数量级。
35 |
36 | 为了能够在不丢失的情况下,处理这些数量的数据,我们必须将它们分成代表信息片段的块。 在 JavaScript 环境中,这些块称为值。 虽然所有值都是由位构成的,但他们起到不同的作用,每个值都有一个决定其作用的类型。 有些值是数字,有些值是文本片段,有些值是函数,等等。
37 |
38 | 要创建一个值,你只需要调用它的名字。 这很方便。 你不必为你的值收集建筑材料或为其付费。 你只需要调用它,然后刷的一下,你就有了它。 当然,它们并不是真正凭空创造的。 每个值都必须存储在某个地方,如果你想同时使用大量的值,则可能会耗尽内存。 幸运的是,只有同时需要它们时,这才是一个问题。 只要你不再使用值,它就会消失,留下它的一部分作为下一代值的建筑材料。
39 |
40 | 本章将会介绍 JavaScript 程序当中的基本元素,包括简单的值类型以及值运算符。
41 |
42 | ## 数字
43 |
44 | 数字(`Number`)类型的值即数字值。在 JavaScript 中写成如下形式:
45 |
46 | ```js
47 | 13
48 | ```
49 |
50 | 在程序中使用这个值的时候,就会将数字 13 以位序列的方式存放在计算机的内存当中。
51 |
52 | JavaScript使用固定数量的位(64 位)来存储单个数字值。 你可以用 64 位创造很多模式,这意味着可以表示的不同数值是有限的。 对于`N`个十进制数字,可以表示的数值数量是`10^N`。 与之类似,给定 64 个二进制数字,你可以表示`2^64`个不同的数字,大约 18 亿亿(18 后面有 18 个零)。太多了。
53 |
54 | 过去计算机内存很小,人们倾向于使用一组 8 位或 16 位来表示他们的数字。 这么小的数字很容易意外地溢出,最终得到的数字不能放在给定的位数中。 今天,即使是装在口袋里的电脑也有足够的内存,所以你可以自由使用 64 位的块,只有在处理真正的天文数字时才需要担心溢出。
55 |
56 | 不过,并非所有 18 亿亿以下的整数都能放在 JavaScript 数值中。 这些位也存储负数,所以一位用于表示数字的符号。 一个更大的问题是,也必须表示非整数。 为此,一些位用于存储小数点的位置。 可以存储的实际最大整数更多地在 9000 万亿(15 个零)的范围内 - 这仍然相当多。
57 |
58 | 使用小数点来表示分数。
59 |
60 | ```js
61 | 9.81
62 | ```
63 |
64 | 对于非常大或非常小的数字,你也可以通过输入`e`(表示指数),后面跟着指数来使用科学记数法:
65 |
66 | ```js
67 | 2.998e8
68 | ```
69 |
70 | 即`2.998 * 10^8 = 299,800,000`。
71 |
72 | 当计算小于前文当中提到的 9000 万亿的整数时,其计算结果会十分精确,不过在计算小数的时候精度却不高。正如(`pi`)无法使用有限个数的十进制数字表示一样,在使用 64 位来存储分数时也同样会丢失一些精度。虽说如此,但这类丢失精度只会在一些特殊情况下才会出现问题。因此我们需要注意在处理分数时,将其视为近似值,而非精确值。
73 |
74 | ### 算术
75 |
76 | 与数字密切相关的就是算术。比如,加法或者乘法之类的算术运算会使用两个数值,并产生一个新的数字。JavaScript 中的算术运算如下所示:
77 |
78 | ```js
79 | 100 + 4 * 11
80 | ```
81 |
82 | 我们把`+`和`*`符号称为运算符。第一个符号表示加法,第二个符号表示乘法。将一个运算符放在两个值之间,该运算符将会使用其旁边的两个值产生一个新值。
83 |
84 | 但是这个例子的意思是“将 4 和 100 相加,并将结果乘 11”,还是是在加法之前计算乘法? 正如你可能猜到的那样,乘法首先计算。 但是和数学一样,你可以通过将加法包在圆括号中来改变它:
85 |
86 | ```js
87 | (100 + 4) * 11
88 | ```
89 |
90 | `–`运算符表示减法,`/`运算符则表示除法。
91 |
92 | 在运算符同时出现,并且没有括号的情况下,其运算顺序根据运算符优先级确定。示例中的乘法运算符优先级高于加法。而`/`运算符和`*`运算符优先级相同,`+`运算符和`–`运算符优先级也相同。当多个具有相同优先级的运算符相邻出现时,运算从左向右执行,比如`1–2+1`的运算顺序是`(1–2)+1`。
93 |
94 | 你无需担心这些运算符的优先级规则,不确定的时候只需要添加括号即可。
95 |
96 | 还有一个算术运算符,你可能无法立即认出。 `%`符号用于表示取余操作。 `X % Y`是`Y`除`X`的余数。 例如,`314 % 100`产生`14`,`144 % 12`产生`0`。 余数的优先级与乘法和除法的优先级相同。 你还经常会看到这个运算符被称为模运算符。
97 |
98 | ### 特殊数字
99 |
100 | 在 JavaScript 中有三个特殊的值,它们虽然是数字,但看起来却跟一般的数字不太一样。
101 |
102 | 前两个是`Infinity`和`-Infinity`,它们代表正无穷和负无穷。 “无穷减一”仍然是“无穷”,依此类推。 尽管如此,不要过分信任基于无穷大的计算。 它在数学上不合理,并且很快导致我们的下一个特殊数字:`NaN`。
103 |
104 | `NaN`代表“不是数字”,即使它是数字类型的值。 例如,当你尝试计算`0/0`(零除零),`Infinity - Infinity`或任何其他数字操作,它不会产生有意义的结果时,你将得到此结果。
105 |
106 | ## 字符串
107 |
108 | 下一个基本数据类型是字符串(`String`)。 字符串用于表示文本。 它们是用引号括起来的:
109 |
110 | ```js
111 | `Down on the sea`
112 | "Lie on the ocean"
113 | 'Float on the ocean'
114 | ```
115 |
116 | 只要字符串开头和结尾的引号匹配,就可以使用单引号,双引号或反引号来标记字符串。
117 |
118 | 几乎所有的东西都可以放在引号之间,并且 JavaScript 会从中提取字符串值。 但少数字符更难。 你可能难以想象,如何在引号之间加引号。 当使用反引号(`` ` ``)引用字符串时,换行符(当你按回车键时获得的字符)可能会被包含,而无需转义。
119 |
120 | 若要将这些字符存入字符串,需要使用下列规则:当反斜杠(`\`)出现在引号之间的文本中时,表示紧跟在其后的字符具有特殊含义,我们将其称之为转义符。当引号紧跟在反斜杠后时,并不意味着字符串结束,而表示这个引号是字符串的一部分。当字符`n`出现在反斜杠后时,JavaScript 将其解释成换行符。以此类推,`\t`表示制表符,我们来看看下面这个字符串:
121 |
122 | ```js
123 | "This is the first line\nAnd this is the second"
124 | ```
125 |
126 | 该字符串实际表示的文本是:
127 |
128 | ```
129 | This is the first line
130 | And this is the second
131 | ```
132 |
133 | 当然,在某些情况下,你希望字符串中的反斜杠只是反斜杠,而不是特殊代码。 如果两个反斜杠写在一起,它们将合并,并且只有一个将留在结果字符串值中。 这就是字符串“`A newline character is written like "\n".`”的表示方式:
134 |
135 | ```js
136 | "A newline character is written like \"\\n\"."
137 | ```
138 |
139 | 字符串也必须建模为一系列位,以便能够存在于计算机内部。 JavaScript 执行此操作的方式基于 Unicode 标准。 该标准为你几乎需要的每个字符分配一个数字,包括来自希腊语,阿拉伯语,日语,亚美尼亚语,以及其他的字符。 如果我们为每个字符分配一个数字,则可以用一系列数字来描述一个字符串。
140 |
141 | 这就是 JavaScript 所做的。 但是有一个复杂的问题:JavaScript 的表示为每个字符串元素使用 16 位,它可以描述多达 2 的 16 次方个不同的字符。 但是,Unicode 定义的字符多于此 - 大约是此处的两倍。 所以有些字符,比如许多 emoji,在 JavaScript 字符串中占据了两个“字符位置”。 我们将在第 5 章中回来讨论。
142 |
143 | 我们不能将除法,乘法或减法运算符用于字符串,但是`+`运算符却可以。这种情况下,运算符并不表示加法,而是连接操作:将两个字符串连接到一起。以下语句可以产生字符串`"concatenate"`:
144 |
145 | ```js
146 | "con" + "cat" + "e" + "nate"
147 | ```
148 |
149 | 字符串值有许多相关的函数(方法),可用于对它们执行其他操作。 我们将在第 4 章中回来讨论。
150 |
151 | 用单引号或双引号编写的字符串的行为非常相似 - 唯一的区别是需要在其中转义哪种类型的引号。 反引号字符串,通常称为模板字面值,可以实现更多的技巧。 除了能够跨越行之外,它们还可以嵌入其他值。
152 |
153 | ```js
154 | `half of 100 is ${100 / 2}`
155 | ```
156 |
157 | 当你在模板字面值中的`$ {}`中写入内容时,将计算其结果,转换为字符串并包含在该位置。 这个例子产生`"half of 100 is 50"`。
158 |
159 | ## 一元运算符
160 |
161 | 并非所有的运算符都是用符号来表示,还有一些运算符是用单词表示的。比如`typeof`运算符,会产生一个字符串的值,内容是给定值的具体类型。
162 |
163 | ```js
164 | console.log(typeof 4.5)
165 | // → number
166 | console.log(typeof "x")
167 | // → string
168 | ```
169 |
170 | 我们将在示例代码中使用`console.log`,来表示我们希望看到求值结果。更多内容请见下一章。
171 |
172 | 我们所见过的绝大多数运算符都使用两个值进行操作,而`typeof`仅接受一个值进行操作。使用两个值的运算符称为二元运算符,而使用一个值的则称为一元运算符。减号运算符既可用作一元运算符,也可用作二元运算符。
173 |
174 | ```js
175 | console.log(- (10 - 2))
176 | // → -8
177 | ```
178 |
179 | ## 布尔值
180 |
181 | 拥有一个值,它能区分两种可能性,通常是有用的,例如“是”和“否”或“开”和“关”。 为此,JavaScript 拥有布尔(`Boolean`)类型,它有两个值:`true`和`false`,它们就写成这些单词。
182 |
183 | ### 比较
184 |
185 | 一种产生布尔值的方法如下所示:
186 |
187 | ```js
188 | console.log(3 > 2)
189 | // → true
190 | console.log(3 < 2)
191 | // → false
192 | ```
193 |
194 | `>`和`<`符号分别表示“大于”和“小于”。这两个符号是二元运算符,通过该运算符返回的结果是一个布尔值,表示其运算是否为真。
195 |
196 | 我们可以使用相同的方法比较字符串。
197 |
198 | ```js
199 | console.log("Aardvark" < "Zoroaster")
200 | // → true
201 | ```
202 |
203 | 字符串排序的方式大致是字典序,但不真正是你期望从字典中看到的那样:大写字母总是比小写字母“小”,所以`"Z"<"a"`,非字母字符(`!`,`-`等)也包含在排序中。 比较字符串时,JavaScript 从左向右遍历字符,逐个比较 Unicode 代码。
204 |
205 | 其他类似的运算符则包括`>=`(大于等于),`<=`(小于等于),`==`(等于)和`!=`(不等于)。
206 |
207 | ```js
208 | console.log("Apple" == "Orange")
209 | // → false
210 | ```
211 |
212 | 在 JavaScript 中,只有一个值不等于其自身,那就是`NaN`(Not a Number,非数值)。
213 |
214 | ```js
215 | console.log(NaN == NaN)
216 | // → false
217 | ```
218 |
219 | `NaN`用于表示非法运算的结果,正因如此,不同的非法运算结果也不会相等。
220 |
221 | ### 逻辑运算符
222 |
223 | 还有一些运算符可以应用于布尔值上。JavaScript 支持三种逻辑运算符:与(and),或(or)和非(not)。这些运算符可以用于推理布尔值。
224 |
225 | `&&`运算符表示逻辑与,该运算符是二元运算符,只有当赋给它的两个值均为`true`时其结果才是真。
226 |
227 | ```js
228 | console.log(true && false)
229 | // → false
230 | console.log(true && true)
231 | // → true
232 | ```
233 |
234 | `||`运算符表示逻辑或。当两个值中任意一个为`true`时,结果就为真。
235 |
236 | ```js
237 | console.log(false || true)
238 | // → true
239 | console.log(false || false)
240 | // → false
241 | ```
242 |
243 | 感叹号(`!`)表示逻辑非,该运算符是一元运算符,用于反转给定的值,比如`!true`的结果是`false`,而`!false`结果是`true`。
244 |
245 | 在混合使用布尔运算符和其他运算符的情况下,总是很难确定什么时候需要使用括号。实际上,只要熟悉了目前为止我们介绍的运算符,这个问题就不难解决了。`||`优先级最低,其次是`&&`,接着是比较运算符(`>`,`==`等),最后是其他运算符。基于这些优先级顺序,我们在一般情况下最好还是尽量少用括号,比如说:
246 |
247 | ```js
248 | 1 + 1 == 2 && 10 * 10 > 50
249 | ```
250 |
251 | 现在我们来讨论最后一个逻辑运算符,它既不属于一元运算符,也不属于二元运算符,而是三元运算符(同时操作三个值)。该运算符由一个问号和冒号组成,如下所示。
252 |
253 | ```js
254 | console.log(true ? 1 : 2);
255 | // → 1
256 | console.log(false ? 1 : 2);
257 | // → 2
258 | ```
259 |
260 | 这个被称为条件运算符(或者有时候只是三元运算符,因为它是该语言中唯一的这样的运算符)。 问号左侧的值“挑选”另外两个值中的一个。 当它为真,它选择中间的值,当它为假,则是右边的值。
261 |
262 | ## 空值
263 |
264 | 有两个特殊值,写成`null`和`undefined`,用于表示不存在有意义的值。 它们本身就是值,但它们没有任何信息。
265 |
266 | 在 JavaScript 语言中,有许多操作都会产生无意义的值(我们会在后面的内容中看到实例),这些操作会得到`undefined`的结果仅仅只是因为每个操作都必须产生一个值。
267 |
268 | `undefined`和`null`之间的意义差异是 JavaScript 设计的一个意外,大多数时候它并不重要。 在你实际上不得不关注这些值的情况下,我建议将它们视为几乎可互换的。
269 |
270 | ## 自动类型转换
271 |
272 | 在引言中,我提到 JavaScript 会尽可能接受几乎所有你给他的程序,甚至是那些做些奇怪事情的程序。 以下表达式很好地证明了这一点:
273 |
274 | ```js
275 | console.log(8 * null)
276 | // → 0
277 | console.log("5" - 1)
278 | // → 4
279 | console.log("5" + 1)
280 | // → 51
281 | console.log("five" * 2)
282 | // → NaN
283 | console.log(false == 0)
284 | // → true
285 | ```
286 |
287 | 当运算符应用于类型“错误”的值时,JavaScript 会悄悄地将该值转换为所需的类型,并使用一组通常不是你想要或期望的规则。 这称为类型转换。 第一个表达式中的`null`变为`0`,第二个表达式中的`"5"`变为`5`(从字符串到数字)。 然而在第三个表达式中,`+`在数字加法之前尝试字符串连接,所以`1`被转换为`"1"`(从数字到字符串)。
288 |
289 | 当某些不能明显映射为数字的东西(如`"five"`或`undefined`)转换为数字时,你会得到值`NaN`。 `NaN`进一步的算术运算会产生`NaN`,所以如果你发现自己在一个意想不到的地方得到了它,需要寻找意外的类型转换。
290 |
291 | 当相同类型的值之间使用`==`符号进行比较时,其运算结果很好预测:除了`NaN`这种情况,只要两个值相同,则返回`true`。但如果类型不同,JavaScript 则会使用一套复杂难懂的规则来确定输出结果。在绝大多数情况下,JavaScript 只是将其中一个值转换成另一个值的类型。但如果运算符两侧存在`null`或`undefined`,那么只有两侧均为`null`或`undefined`时结果才为`true`。
292 |
293 | ```js
294 | console.log(null == undefined);
295 | // → true
296 | console.log(null == 0);
297 | // → false
298 | ```
299 |
300 | 这种行为通常很有用。 当你想测试一个值是否具有真值而不是`null`或`undefined`时,你可以用`==`(或`!=`)运算符将它与`null`进行比较。
301 |
302 | 但是如果你想测试某些东西是否严格为“false”呢? 字符串和数字转换为布尔值的规则表明,`0`,`NaN`和空字符串(`""`)计为`false`,而其他所有值都计为`true`。 因此,像`'0 == false'`和`"" == false`这样的表达式也是真的。 当你不希望发生自动类型转换时,还有两个额外的运算符:`===`和`!==`。 第一个测试是否严格等于另一个值,第二个测试它是否不严格相等。 所以`"" === false`如预期那样是错误的。
303 |
304 | 我建议使用三字符比较运算符来防止意外类型转换的发生,避免作茧自缚。但如果比较运算符两侧的值类型是相同的,那么使用较短的运算符也没有问题。
305 |
306 | ### 逻辑运算符的短路特性
307 |
308 | 逻辑运算符`&&`和`||`以一种特殊的方式处理不同类型的值。 他们会将其左侧的值转换为布尔型,来决定要做什么,但根据运算符和转换结果,它们将返回原始的左侧值或右侧值。
309 |
310 | 例如,当左侧值可以转换为`true`时,`||`运算符会返回它,否则返回右侧值。 当值为布尔值时,这具有预期的效果,并且对其他类型的值做类似的操作。
311 |
312 | ```js
313 | console.log(null || "user")
314 | // → user
315 | console.log("Agnes" || "user")
316 | // → Agnes
317 | ```
318 |
319 | 我们可以此功能用作回落到默认值的方式。 如果你的一个值可能是空的,你可以把`||`和备选值放在它之后。 如果初始值可以转换为`false`,那么你将得到备选值。
320 |
321 | `&&`运算符工作方式与其相似但不相同。当左侧的值可以被转换成`false`时,`&&`运算符会返回左侧值,否则返回右侧值。
322 |
323 | 这两个运算符的另一个重要特性是,只在必要时求解其右侧的部分。 在`true || X`的情况下,不管`X`是什么 - 即使它是一个执行某些恶意操作的程序片段,结果都是`true`,并且`X`永远不会求值。 `false && X`也是一样,它是`false`的,并且忽略`X`。 这称为短路求值。
324 |
325 | 条件运算符以类似的方式工作。 在第二个和第三个值中,只有被选中的值才会求值。
326 |
327 | ## 本章小结
328 |
329 | 在本章中,我们介绍了 JavaScript 的四种类型的值:数字,字符串,布尔值和未定义值。
330 |
331 | 通过输入值的名称(`true`,`null`)或值(`13`,`"abc"`)就可以创建它们。你还可以通过运算符来对值进行合并和转换操作。本章已经介绍了算术二元运算符(`+`,`–`,`*`,`/`和`%`),字符串连接符(`+`),比较运算符(`==`,`!=`,`===`,`!==`,`<`,`>`,`<=`和`>=`),逻辑运算符(`&&`和`||`)和一些一元运算符(`–`表示负数,`!`表示逻辑非,`typeof`用于查询值的类型)。
332 |
333 | 这为你提供了足够的信息,将 JavaScript 用作便携式计算器,但并不多。 下一章将开始将这些表达式绑定到基本程序中。
334 |
--------------------------------------------------------------------------------
/10.md:
--------------------------------------------------------------------------------
1 | # 十、模块
2 |
3 | > 原文:[Modules](http://eloquentjavascript.net/10_modules.html)
4 | >
5 | > 译者:[飞龙](https://github.com/wizardforcel)
6 | >
7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
8 | >
9 | > 自豪地采用[谷歌翻译](https://translate.google.cn/)
10 |
11 | > 编写易于删除,而不是易于扩展的代码。
12 | >
13 | > Tef,《Programming is Terrible》
14 |
15 | 
16 |
17 | 理想的程序拥有清晰的结构。 它的工作方式很容易解释,每个部分都起到明确的作用。
18 |
19 | 典型的真实程序会有机地增长。 新功能随着新需求的出现而增加。 构建和维护结构是额外的工作,只有在下一次有人参与该计划时,才会得到回报。 所以它易于忽视,并让程序的各个部分变得深深地纠缠在一起。
20 |
21 | 这导致了两个实际问题。 首先,这样的系统难以理解。 如果一切都可以接触到一切其它东西,那么很难单独观察任何给定的片段。 你不得不全面理解整个东西。 其次,如果你想在另一个场景中,使用这种程序的任何功能,比起试图从它的上下文中将它分离出来,重写它可能要容易。
22 |
23 | 术语“大泥球”通常用于这种大型,无结构的程序。 一切都粘在一起,当你试图挑选出一段代码时,整个东西就会分崩离析,你的手会变脏。
24 |
25 | ## 模块
26 |
27 | 模块试图避免这些问题。 模块是一个程序片段,规定了它依赖的其他部分,以及它为其他模块提供的功能(它的接口)。
28 |
29 | 模块接口与对象接口有许多共同之处,我们在第 6 章中看到。它们向外部世界提供模块的一部分,并使其余部分保持私有。 通过限制模块彼此交互的方式,系统变得更像积木,其中的组件通过明确定义的连接器进行交互,而不像泥浆一样,一切都混在一起。
30 |
31 | 模块之间的关系称为依赖关系。 当一个模块需要另一个模块的片段时,就说它依赖于这个模块。 当模块中明确规定了这个事实时,它可以用于确定,需要哪些其他模块才能使用给定的模块,并自动加载依赖关系。
32 |
33 | 为了以这种方式分离模块,每个模块需要它自己的私有作用域。
34 |
35 | 将你的 JavaScript 代码放入不同的文件,不能满足这些要求。 这些文件仍然共享相同的全局命名空间。 他们可以有意或无意干扰彼此的绑定。 依赖性结构仍不清楚。 我们将在本章后面看到,我们可以做得更好。
36 |
37 | 合适的模块结构可能难以为程序设计。 在你还在探索这个问题的阶段,尝试不同的事情来看看什么是可行的,你可能不想过多担心它,因为这可能让你分心。 一旦你有一些感觉可靠的东西,现在是后退一步并组织它的好时机。
38 |
39 | ## 包
40 |
41 | 从单独的片段中构建一个程序,并实际上能够独立运行这些片段的一个优点是,你可能能够在不同的程序中应用相同的部分。
42 |
43 | 但如何实现呢? 假设我想在另一个程序中使用第 9 章中的`parseINI`函数。 如果清楚该函数依赖什么(在这种情况下什么都没有),我可以将所有必要的代码复制到我的新项目中并使用它。 但是,如果我在代码中发现错误,我可能会在当时正在使用的任何程序中将其修复,并忘记在其他程序中修复它。
44 |
45 | 一旦你开始复制代码,你很快就会发现,自己在浪费时间和精力来到处复制并使他们保持最新。
46 |
47 | 这就是包的登场时机。包是可分发(复制和安装)的一大块代码。 它可能包含一个或多个模块,并且具有关于它依赖于哪些其他包的信息。 一个包通常还附带说明它做什么的文档,以便那些不编写它的人仍然可以使用它。
48 |
49 | 在包中发现问题或添加新功能时,会将包更新。 现在依赖它的程序(也可能是包)可以升级到新版本。
50 |
51 | 以这种方式工作需要基础设施。 我们需要一个地方来存储和查找包,以及一个便利方式来安装和升级它们。 在 JavaScript 世界中,这个基础结构由 [NPM](https://npmjs.org) 提供。
52 |
53 | NPM 是两个东西:可下载(和上传)包的在线服务,以及可帮助你安装和管理它们的程序(与 Node.js 捆绑在一起)。
54 |
55 | 在撰写本文时,NPM 上有超过 50 万个不同的包。 其中很大一部分是垃圾,我应该提一下,但几乎所有有用的公开包都可以在那里找到。 例如,一个 INI 文件解析器,类似于我们在第 9 章中构建的那个,可以在包名称`ini`下找到。
56 |
57 | 第 20 章将介绍如何使用`npm`命令行程序在局部安装这些包。
58 |
59 | 使优质的包可供下载是非常有价值的。 这意味着我们通常可以避免重新创建一百人之前写过的程序,并在按下几个键时得到一个可靠,充分测试的实现。
60 |
61 | 软件的复制很便宜,所以一旦有人编写它,分发给其他人是一个高效的过程。但首先把它写出来是工作量,回应在代码中发现问题的人,或者想要提出新功能的人,是更大的工作量。
62 |
63 | 默认情况下,你拥有你编写的代码的版权,其他人只有经过你的许可才能使用它。但是因为有些人不错,而且由于发布好的软件可以使你在程序员中出名,所以许多包都会在许可证下发布,明确允许其他人使用它。
64 |
65 | NPM 上的大多数代码都以这种方式授权。某些许可证要求你还要在相同许可证下发布基于那个包构建的代码。其他要求不高,只是要求在分发代码时保留许可证。 JavaScript 社区主要使用后一种许可证。使用其他人的包时,请确保你留意了他们的许可证。
66 |
67 | ## 即兴的模块
68 |
69 | 2015 年之前,JavaScript 语言没有内置的模块系统。 然而,尽管人们已经用 JavaScript 构建了十多年的大型系统,他们需要模块。
70 |
71 | 所以他们在语言之上设计了自己的模块系统。 你可以使用 JavaScript 函数创建局部作用域,并使用对象来表示模块接口。
72 |
73 | 这是一个模块,用于日期名称和数字之间的转换(由`Date`的`getDay`方法返回)。 它的接口由`weekDay.name`和`weekDay.number`组成,它将局部绑定名称隐藏在立即调用的函数表达式的作用域内。
74 |
75 | ```js
76 | const weekDay = function() {
77 | const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
78 | "Thursday", "Friday", "Saturday"];
79 | return {
80 | name(number) { return names[number]; },
81 | number(name) { return names.indexOf(name); }
82 | };
83 | }();
84 |
85 | console.log(weekDay.name(weekDay.number("Sunday")));
86 | // → Sunday
87 | ```
88 |
89 | 这种风格的模块在一定程度上提供了隔离,但它不声明依赖关系。 相反,它只是将其接口放入全局范围,并希望它的依赖关系(如果有的话)也这样做。 很长时间以来,这是 Web 编程中使用的主要方法,但现在它几乎已经过时。
90 |
91 | 如果我们想让依赖关系成为代码的一部分,我们必须控制依赖关系的加载。 实现它需要能够将字符串执行为代码。 JavaScript 可以做到这一点。
92 |
93 | ## 将数据执行为代码
94 |
95 | 有几种方法可以将数据(代码的字符串)作为当前程序的一部分运行。
96 |
97 | 最明显的方法是特殊运算符`eval`,它将在当前作用域内执行一个字符串。 这通常是一个坏主意,因为它破坏了作用域通常拥有的一些属性,比如易于预测给定名称所引用的绑定。
98 |
99 | ```js
100 | const x = 1;
101 | function evalAndReturnX(code) {
102 | eval(code);
103 | return x;
104 | }
105 |
106 | console.log(evalAndReturnX("var x = 2"));
107 | // → 2
108 | console.log(x);
109 | // → 1
110 | ```
111 |
112 | 将数据解释为代码的不太可怕的方法,是使用`Function`构造器。 它有两个参数:一个包含逗号分隔的参数名称列表的字符串,和一个包含函数体的字符串。 它将代码封装在一个函数值中,以便它获得自己的作用域,并且不会对其他作用域做出奇怪的事情。
113 |
114 | ```py
115 | let plusOne = Function("n", "return n + 1;");
116 | console.log(plusOne(4));
117 | // → 5
118 | ```
119 |
120 | 这正是我们需要的模块系统。 我们可以将模块的代码包装在一个函数中,并将该函数的作用域用作模块作用域。
121 |
122 | ## CommonJS
123 |
124 | 用于连接 JavaScript 模块的最广泛的方法称为 CommonJS 模块。 Node.js 使用它,并且是 NPM 上大多数包使用的系统。
125 |
126 | CommonJS 模块的主要概念是称为`require`的函数。 当你使用依赖项的模块名称调用这个函数时,它会确保该模块已加载并返回其接口。
127 |
128 | 由于加载器将模块代码封装在一个函数中,模块自动得到它们自己的局部作用域。 他们所要做的就是,调用`require`来访问它们的依赖关系,并将它们的接口放在绑定到`exports`的对象中。
129 |
130 | 此示例模块提供了日期格式化功能。 它使用 NPM的两个包,`ordinal`用于将数字转换为字符串,如`"1st"`和`"2nd"`,以及`date-names`用于获取星期和月份的英文名称。 它导出函数`formatDate`,它接受一个`Date`对象和一个模板字符串。
131 |
132 | 模板字符串可包含指明格式的代码,如`YYYY`用于全年,`Do`用于每月的序数日。 你可以给它一个像`"MMMM Do YYYY"`这样的字符串,来获得像`"November 22nd 2017"`这样的输出。
133 |
134 | ```js
135 | const ordinal = require("ordinal");
136 | const {days, months} = require("date-names");
137 |
138 | exports.formatDate = function(date, format) {
139 | return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
140 | if (tag == "YYYY") return date.getFullYear();
141 | if (tag == "M") return date.getMonth();
142 | if (tag == "MMMM") return months[date.getMonth()];
143 | if (tag == "D") return date.getDate();
144 | if (tag == "Do") return ordinal(date.getDate());
145 | if (tag == "dddd") return days[date.getDay()];
146 | });
147 | };
148 | ```
149 |
150 | `ordinal`的接口是单个函数,而`date-names`导出包含多个东西的对象 - `days`和`months`是名称数组。 为导入的接口创建绑定时,解构是非常方便的。
151 |
152 | 该模块将其接口函数添加到`exports`,以便依赖它的模块可以访问它。 我们可以像这样使用模块:
153 |
154 | ```js
155 | const {formatDate} = require("./format-date");
156 |
157 | console.log(formatDate(new Date(2017, 9, 13),
158 | "dddd the Do"));
159 | // → Friday the 13th
160 | ```
161 |
162 | 我们可以用最简单的形式定义`require`,如下所示:
163 |
164 | ```js
165 | require.cache = Object.create(null);
166 |
167 | function require(name) {
168 | if (!(name in require.cache)) {
169 | let code = readFile(name);
170 | let module = {exports: {}};
171 | require.cache[name] = module;
172 | let wrapper = Function("require, exports, module", code);
173 | wrapper(require, module.exports, module);
174 | }
175 | return require.cache[name].exports;
176 | }
177 | ```
178 |
179 | 在这段代码中,`readFile`是一个构造函数,它读取一个文件并将其内容作为字符串返回。标准的 JavaScript 没有提供这样的功能,但是不同的 JavaScript 环境(如浏览器和 Node.js)提供了自己的访问文件的方式。这个例子只是假设`readFile`存在。
180 |
181 | 为了避免多次加载相同的模块,`require`需要保存(缓存)已经加载的模块。被调用时,它首先检查所请求的模块是否已加载,如果没有,则加载它。这涉及到读取模块的代码,将其包装在一个函数中,然后调用它。
182 |
183 | 我们之前看到的`ordinal`包的接口不是一个对象,而是一个函数。 CommonJS 模块的特点是,尽管模块系统会为你创建一个空的接口对象(绑定到`exports`),但你可以通过覆盖`module.exports`来替换它。许多模块都这么做,以便导出单个值而不是接口对象。
184 |
185 | 通过将`require`,`exports`和`module`定义为生成的包装函数的参数(并在调用它时传递适当的值),加载器确保这些绑定在模块的作用域中可用。
186 |
187 | 提供给`require`的字符串翻译为实际的文件名或网址的方式,在不同系统有所不同。 当它以`"./"`或`"../"`开头时,它通常被解释为相对于当前模块的文件名。 所以`"./format-date"`就是在同一个目录中,名为`format-date.js`的文件。
188 |
189 | 当名称不是相对的时,Node.js 将按照该名称查找已安装的包。 在本章的示例代码中,我们将把这些名称解释为 NPM 包的引用。 我们将在第 20 章详细介绍如何安装和使用 NPM 模块。
190 |
191 | 现在,我们不用编写自己的 INI 文件解析器,而是使用 NPM 中的某个:
192 |
193 | ```js
194 | const {parse} = require("ini");
195 |
196 | console.log(parse("x = 10\ny = 20"));
197 | // → {x: "10", y: "20"}
198 | ```
199 |
200 | ## ECMAScript 模块
201 |
202 | CommonJS 模块很好用,并且与 NPM 一起,使 JavaScript 社区开始大规模共享代码。
203 |
204 | 但他们仍然是个简单粗暴的黑魔法。 例如,表示法有点笨拙 - 添加到`exports`的内容在局部作用域中不可用。 而且因为`require`是一个正常的函数调用,接受任何类型的参数,而不仅仅是字符串字面值,所以在不运行代码就很难确定模块的依赖关系。
205 |
206 | 这就是 2015 年的 JavaScript 标准引入了自己的不同模块系统的原因。 它通常被称为 ES 模块,其中 ES 代表 ECMAScript。 依赖和接口的主要概念保持不变,但细节不同。 首先,表示法现在已整合到该语言中。 你不用调用函数来访问依赖关系,而是使用特殊的`import`关键字。
207 |
208 | ```js
209 | import ordinal from "ordinal";
210 | import {days, months} from "date-names";
211 |
212 | export function formatDate(date, format) { /* ... */ }
213 | ```
214 |
215 | 同样,`export`关键字用于导出东西。 它可以出现在函数,类或绑定定义(`let`,`const`或`var`)的前面。
216 |
217 | ES 模块的接口不是单个值,而是一组命名绑定。 前面的模块将`formatDate`绑定到一个函数。 从另一个模块导入时,导入绑定而不是值,这意味着导出模块可以随时更改绑定的值,导入它的模块将看到其新值。
218 |
219 | 当有一个名为`default`的绑定时,它将被视为模块的主要导出值。 如果你在示例中导入了一个类似于`ordinal`的模块,而没有绑定名称周围的大括号,则会获得其默认绑定。 除了默认绑定之外,这些模块仍然可以以不同名称导出其他绑定。
220 |
221 | 为了创建默认导出,可以在表达式,函数声明或类声明之前编写`export default`。
222 |
223 | ```js
224 | export default ["Winter", "Spring", "Summer", "Autumn"];
225 | ```
226 |
227 | 可以使用单词`as`重命名导入的绑定。
228 |
229 | ```js
230 | import {days as dayNames} from "date-names";
231 |
232 | console.log(dayNames.length);
233 | // → 7
234 | ```
235 |
236 | 另一个重要的区别是,ES 模块的导入发生在模块的脚本开始运行之前。 这意味着`import`声明可能不会出现在函数或块中,并且依赖项的名称只能是带引号的字符串,而不是任意的表达式。
237 |
238 | 在撰写本文时,JavaScript 社区正在采用这种模块风格。 但这是一个缓慢的过程。 在规定格式之后,花了几年的时间,浏览器和 Node.js 才开始支持它。 虽然他们现在几乎都支持它,但这种支持仍然存在问题,这些模块如何通过 NPM 分发的讨论仍在进行中。
239 |
240 | 许多项目使用 ES 模块编写,然后在发布时自动转换为其他格式。 我们正处于并行使用两个不同模块系统的过渡时期,并且能够读写任何一种之中的代码都很有用。
241 |
242 | ## 构建和打包
243 |
244 | 事实上,从技术上来说,许多 JavaScript 项目都不是用 JavaScript 编写的。有一些扩展被广泛使用,例如第 8 章中提到的类型检查方言。很久以前,在语言的某个计划性扩展添加到实际运行 JavaScript 的平台之前,人们就开始使用它了。
245 |
246 | 为此,他们编译他们的代码,将其从他们选择的 JavaScript 方言翻译成普通的旧式 JavaScript,甚至是过去的 JavaScript 版本,以便旧版浏览器可以运行它。
247 |
248 | 在网页中包含由 200 个不同文件组成的模块化程序,会产生它自己的问题。如果通过网络获取单个文件需要 50 毫秒,则加载整个程序需要 10 秒,或者如果可以同时加载多个文件,则可能需要一半。这浪费了很多时间。因为抓取一个大文件往往比抓取很多小文件要快,所以 Web 程序员已经开始使用工具,将它们发布到 Web 之前,将他们(费力分割成模块)的程序回滚成单个大文件。这些工具被称为打包器。
249 |
250 | 我们可以再深入一点。 除了文件的数量之外,文件的大小也决定了它们可以通过网络传输的速度。 因此,JavaScript 社区发明了压缩器。 通过自动删除注释和空白,重命名绑定以及用占用更少空间的等效代码替换代码段,这些工具使 JavaScript 程序变得更小。
251 |
252 | 因此,你在 NPM 包中找到的代码,或运行在网页上的代码,经历了多个转换阶段 - 从现代 JavaScript 转换为历史 JavaScript,从 ES 模块格式转换为 CommonJS,打包并压缩。 我们不会在本书中详细介绍这些工具,因为它们往往很无聊,并且变化很快。 请注意,你运行的 JavaScript 代码通常不是编写的代码。
253 |
254 | ## 模块设计
255 |
256 | 使程序结构化是编程的一个微妙的方面。 任何有价值的功能都可以用各种方式建模。
257 |
258 | 良好的程序设计是主观的 - 涉及到权衡和品味问题。 了解结构良好的设计的价值的最好方法,是阅读或处理大量程序,并注意哪些是有效的,哪些不是。 不要认为一个痛苦的混乱就是“它本来的方式”。 通过多加思考,你可以改善几乎所有事物的结构。
259 |
260 | 模块设计的一个方面是易用性。 如果你正在设计一些旨在由多人使用,或者甚至是你自己的东西,在三个月之内,当你记不住你所做的细节时,如果你的接口简单且可预测,这会有所帮助。
261 |
262 | 这可能意味着遵循现有的惯例。 `ini`包是一个很好的例子。 此模块模仿标准 JSON 对象,通过提供`parse`和`stringify`(用于编写 INI 文件)函数,就像 JSON 一样,在字符串和普通对象之间进行转换。 所以接口很小且很熟悉,在你使用过一次后,你可能会记得如何使用它。
263 |
264 | 即使没有能模仿的标准函数或广泛使用的包,你也可以通过使用简单的数据结构,并执行单一的重点事项,来保持模块的可预测性。 例如,NPM 上的许多 INI 文件解析模块,提供了直接从硬盘读取文件并解析它的功能。 这使得在浏览器中不可能使用这些模块,因为我们没有文件系统的直接访问权,并且增加了复杂性,通过组合模块与某些文件读取功能,可以更好地解决它。
265 |
266 | 这指向了模块设计的另一个有用的方面 - 一些代码可以轻易与其他代码组合。比起执行带有副作用的复杂操作的更大的模块,计算值的核心模块适用于范围更广的程序。坚持从磁盘读取文件的 INI 文件读取器, 在文件内容来自其他来源的场景中是无用的。
267 |
268 | 与之相关,有状态的对象有时甚至是有用的,但是如果某件事可以用一个函数完成,就用一个函数。 NPM 上的几个 INI 文件读取器提供了一种接口风格,需要你先创建一个对象,然后将该文件加载到对象中,最后使用特定方法来获取结果。这种类型的东西在面向对象的传统中很常见,而且很糟糕。你不能调用单个函数来完成,你必须执行仪式,在各种状态中移动对象。而且由于数据现在封装在一个特定的对象类型中,与它交互的所有代码都必须知道该类型,从而产生不必要的相互依赖关系。
269 |
270 | 通常,定义新的数据结构是不可避免的 - 只有少数非常基本的数据结构由语言标准提供,并且许多类型的数据一定比数组或映射更复杂。 但是当数组足够时,使用数组。
271 |
272 | 一个稍微复杂的数据结构的示例是第 7 章的图。JavaScript 中没有一种明显的表示图的方式。 在那一章中,我们使用了一个对象,其属性保存了字符串数组 - 可以从某个节点到达的其他节点。
273 |
274 | NPM 上有几种不同的寻路包,但他们都没有使用这种图的格式。 它们通常允许图的边带有权重,它是与其相关的成本或距离,这在我们的表示中是不可能的。
275 |
276 | 例如,存在`dijkstrajs`包。 一种著名的寻路方法,与我们的`findRoute`函数非常相似,它被称为迪科斯特拉(Dijkstra)算法,以首先编写它的艾兹格尔·迪科斯特拉(Edsger Dijkstra)命名。 `js`后缀通常会添加到包名称中,以表明它们用 JavaScript 编写。 这个`dijkstrajs`包使用类似于我们的图的格式,但是它不使用数组,而是使用对象,它的属性值是数字 - 边的权重。
277 |
278 | 所以如果我们想要使用这个包,我们必须确保我们的图以它期望的格式存储。 所有边的权重都相同,因为我们的简化模型将每条道路视为具有相同的成本(一个回合)。
279 |
280 | ```js
281 | const {find_path} = require("dijkstrajs");
282 |
283 | let graph = {};
284 | for (let node of Object.keys(roadGraph)) {
285 | let edges = graph[node] = {};
286 | for (let dest of roadGraph[node]) {
287 | edges[dest] = 1;
288 | }
289 | }
290 |
291 | console.log(find_path(graph, "Post Office", "Cabin"));
292 | // → ["Post Office", "Alice's House", "Cabin"]
293 | ```
294 |
295 | 这可能是组合的障碍 - 当各种包使用不同的数据结构来描述类似的事情时,将它们组合起来很困难。 因此,如果你想要设计可组合性,请查找其他人使用的数据结构,并在可能的情况下遵循他们的示例。
296 |
297 | ## 总结
298 |
299 | 通过将代码分离成具有清晰接口和依赖关系的块,模块是更大的程序结构。 接口是模块中可以从其他模块看到的部分,依赖关系是它使用的其他模块。
300 |
301 | 由于 JavaScript 历史上并没有提供模块系统,因此 CommonJS 系统建立在它之上。 然后在某个时候,它确实有了一个内置系统,它现在与 CommonJS 系统不兼容。
302 |
303 | 包是可以自行分发的一段代码。 NPM 是 JavaScript 包的仓库。 你可以从上面下载各种有用的(和无用的)包。
304 |
305 | ## 练习
306 |
307 | ### 模块化机器人
308 |
309 | 这些是第 7 章的项目所创建的约束:
310 |
311 | ```
312 | roads
313 | buildGraph
314 | roadGraph
315 | VillageState
316 | runRobot
317 | randomPick
318 | randomRobot
319 | mailRoute
320 | routeRobot
321 | findRoute
322 | goalOrientedRobot
323 | ```
324 |
325 | 如果你要将该项目编写为模块化程序,你会创建哪些模块? 哪个模块依赖于哪个模块,以及它们的接口是什么样的?
326 |
327 | 哪些片段可能在 NPM 上找到? 你愿意使用 NPM 包还是自己编写?
328 |
329 | ### `roads`模块
330 |
331 | 根据第 7 章中的示例编写 CommonJS 模块,该模块包含道路数组,并将表示它们的图数据结构导出为`roadGraph`。 它应该依赖于一个模块`./graph`,它导出一个函数`buildGraph`,用于构建图。 该函数接受包含两个元素的数组(道路的起点和终点)。
332 |
333 | ```js
334 | // Add dependencies and exports
335 |
336 | const roads = [
337 | "Alice's House-Bob's House", "Alice's House-Cabin",
338 | "Alice's House-Post Office", "Bob's House-Town Hall",
339 | "Daria's House-Ernie's House", "Daria's House-Town Hall",
340 | "Ernie's House-Grete's House", "Grete's House-Farm",
341 | "Grete's House-Shop", "Marketplace-Farm",
342 | "Marketplace-Post Office", "Marketplace-Shop",
343 | "Marketplace-Town Hall", "Shop-Town Hall"
344 | ];
345 | ```
346 |
347 | ### 循环依赖
348 |
349 | 循环依赖是一种情况,其中模块 A 依赖于 B,并且 B 也直接或间接依赖于 A。许多模块系统完全禁止这种情况,因为无论你选择何种顺序来加载此类模块,都无法确保每个模块的依赖关系在它运行之前加载。
350 |
351 | CommonJS 模块允许有限形式的循环依赖。 只要这些模块不会替换它们的默认`exports`对象,并且在完成加载之后才能访问对方的接口,循环依赖就没有问题。
352 |
353 | 本章前面给出的`require`函数支持这种类型的循环依赖。 你能看到它如何处理循环吗? 当一个循环中的某个模块替代其默认`exports`对象时,会出现什么问题?
354 |
--------------------------------------------------------------------------------
/12.md:
--------------------------------------------------------------------------------
1 | ## 十二、项目:编程语言
2 |
3 | > 原文:[Project: A Programming Language](https://eloquentjavascript.net/12_language.html)
4 | >
5 | > 译者:[飞龙](https://github.com/wizardforcel)
6 | >
7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
8 | >
9 | > 自豪地采用[谷歌翻译](https://translate.google.cn/)
10 | >
11 | > 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
12 |
13 | > 确定编程语言中的表达式含义的求值器只是另一个程序。
14 | >
15 | > Hal Abelson 和 Gerald Sussman,《计算机程序的构造和解释》
16 |
17 | 
18 |
19 | 构建你自己的编程语言不仅简单(只要你的要求不要太高就好),而且对人富有启发。
20 |
21 | 希望通过本章的介绍,你能发现构建自己的编程语言其实并不是什么难事。我经常感到某些人的想法聪明无比,而且十分复杂,以至于我都不能完全理解。不过经过一段时间的阅读和实验,我就发现它们其实也并没有想象中那么复杂。
22 |
23 | 我们将创造一门名为 Egg 的编程语言。这是一门小巧而简单的语言,但是足够强大到能描述你所能想到的任何计算。它允许基于函数的简单抽象。
24 |
25 | ## 解析
26 |
27 | 程序设计语言中最直观的部分就是语法(syntax)或符号。解析器是一种程序,负责读入文本片段(包含程序的文本),并产生一系列与程序结构对应的数据结构。若文本不是一个合法程序,解析器应该指出错误。
28 |
29 | 我们的语言语法简单,而且具有一致性。Egg 中一切都是表达式。表达式可以是绑定名称、数字,或应用(application)。不仅函数调用属于应用,而且`if`和`while`之类的语言构造也属于应用。
30 |
31 | 为了确保解析器的简单性,Egg 中的字符串不支持反斜杠转义符之类的元素。字符串只是简单的字符序列(不包括双引号),并使用双引号包围起来。数值是数字序列。绑定名由任何非空白字符组成,并且在语法中不具有特殊含义。
32 |
33 | 应用的书写方式与 JavaScript 中一样,也是在一个表达式后添加一对括号,括号中可以包含任意数量的参数,参数之间使用逗号分隔。
34 |
35 | ```egg
36 | do(define(x, 10),
37 | if(>(x, 5),
38 | print("large"),
39 | print("small")))
40 | ```
41 |
42 | Egg 语言的一致性体现在:JavaScript 中的所有运算符(比如`>`)在 Egg 中都是绑定,但是可以像其他函数一样调用。由于语法中没有语句块的概念,因此我们需要使用`do`结构来表示多个表达式的序列。
43 |
44 | 解析器的数据结构用于描述由表达式对象组成的程序,每个对象都包含一个表示表达式类型的`type`属性,除此以外还有其他描述对象内容的属性。
45 |
46 | 类型为`"value"`的表达式表示字符串和数字。它们的`value`属性包含对应的字符串和数字值。类型为`"word"`的表达式用于标识符(名称)。这类对象以字符串形式将标识符名称保存在`name`属性中。最后,类型为`"apply"`的表达式表示应用。该类型的对象有一个`operator`属性,指向其操作的表达式,还有一个`args`属性,持有参数表达式的数组。
47 |
48 | 上面代码中`> (x, 5)`这部分可以表达成如下形式:
49 |
50 | ```js
51 | {
52 | type: "apply",
53 | operator: {type: "word", name: ">"},
54 | args: [
55 | {type: "word", name: "x"},
56 | {type: "value", value: 5}
57 | ]
58 | }
59 | ```
60 |
61 | 我们将这样一个数据结构称为表达式树。如果你将对象想象成点,将对象之间的连接想象成点之间的线,这个数据结构将会变成树形。表达式中还会包含其他表达式,被包含的表达式接着又会包含更多表达式,这类似于树的分支重复分裂的方式。
62 |
63 | 
64 |
65 | 我们将这个解析器与我们第 9 章中编写的配置文件格式解析器进行对比,第 9 章中的解析器结构很简单:将输入文件划分成行,并逐行处理。而且每一行只有几种简单的语法形式。
66 |
67 | 我们必须使用不同方法来解决这里的问题。Egg 中并没有表达式按行分隔,而且表达式之间还有递归结构。应用表达式包含其他表达式。
68 |
69 | 所幸我们可以使用递归的方式编写一个解析器函数,并优雅地解决该问题,这反映了语言自身就是递归的。
70 |
71 | 我们定义了一个函数`parseExpression`,该函数接受一个字符串,并返回一个对象,包含了字符串起始位置处的表达式与解析表达式后剩余的字符串。当解析子表达式时(比如应用的参数),可以再次调用该函数,返回参数表达式和剩余字符串。剩余的字符串可以包含更多参数,也有可以是一个表示参数列表结束的右括号。
72 |
73 | 这里给出部分解析器代码。
74 |
75 | ```js
76 | function parseExpression(program) {
77 | program = skipSpace(program);
78 | let match, expr;
79 | if (match = /^"([^"]*)"/.exec(program)) {
80 | expr = {type: "value", value: match[1]};
81 | } else if (match = /^\d+\b/.exec(program)) {
82 | expr = {type: "value", value: Number(match[0])};
83 | } else if (match = /^[^\s(),#"]+/.exec(program)) {
84 | expr = {type: "word", name: match[0]};
85 | } else {
86 | throw new SyntaxError("Unexpected syntax: " + program);
87 | }
88 |
89 | return parseApply(expr, program.slice(match[0].length));
90 | }
91 |
92 | function skipSpace(string) {
93 | let first = string.search(/\S/);
94 | if (first == -1) return "";
95 | return string.slice(first);
96 | }
97 | ```
98 |
99 | 由于 Egg 和 JavaScript 一样,允许其元素之间有任意数量的空白,所以我们必须在程序字符串的开始处重复删除空白。 这就是`skipSpace`函数能提供的帮助。
100 |
101 | 跳过开头的所有空格后,`parseExpression`使用三个正则表达式来检测 Egg 支持的三种原子的元素:字符串、数值和单词。解析器根据不同的匹配结果构造不同的数据类型。如果这三种形式都无法与输入匹配,那么输入就是一个非法表达式,解析器就会抛出异常。我们使用`SyntaxError`而不是`Error`作为异常构造器,这是另一种标准错误类型,因为它更具体 - 它也是在尝试运行无效的 JavaScript 程序时,抛出的错误类型。
102 |
103 | 接下来,我们从程序字符串中删去匹配的部分,将剩余的字符串和表达式对象一起传递给`parseApply`函数。该函数检查表达式是否是一个应用,如果是应用则解析带括号的参数列表。
104 |
105 | ```js
106 | function parseApply(expr, program) {
107 | program = skipSpace(program);
108 | if (program[0] != "(") {
109 | return {expr: expr, rest: program};
110 | }
111 |
112 | program = skipSpace(program.slice(1));
113 | expr = {type: "apply", operator: expr, args: []};
114 | while (program[0] != ")") {
115 | let arg = parseExpression(program);
116 | expr.args.push(arg.expr);
117 | program = skipSpace(arg.rest);
118 | if (program[0] == ",") {
119 | program = skipSpace(program.slice(1));
120 | } else if (program[0] != ")") {
121 | throw new SyntaxError("Expected ',' or ')'");
122 | }
123 | }
124 | return parseApply(expr, program.slice(1));
125 | }
126 | ```
127 |
128 | 如果程序中的下一个字符不是左圆括号,说明当前表达式不是一个应用,parseApply会返回该表达式。
129 |
130 | 否则,该函数跳过左圆括号,为应用表达式创建语法树。接着递归调用`parseExpression`解析每个参数,直到遇到右圆括号为止。此处通过`parseApply`和`parseExpression`互相调用,实现函数间接递归调用。
131 |
132 | 因为我们可以使用一个应用来操作另一个应用表达式(比如`multiplier(2)(1)`),所以`parseApply`解析完一个应用后必须再次调用自身检查是否还有另一对圆括号。
133 |
134 | 这就是我们解析 Egg 所需的全部代码。我们使用`parse`函数来包装`parseExpression`,在解析完表达式之后验证输入是否到达结尾(一个 Egg 程序是一个表达式),遇到输入结尾后会返回整个程序对应的数据结构。
135 |
136 | ```js
137 | function parse(program) {
138 | let {expr, rest} = parseExpression(program);
139 | if (skipSpace(rest).length > 0) {
140 | throw new SyntaxError("Unexpected text after program");
141 | }
142 | return expr;
143 | }
144 |
145 | console.log(parse("+(a, 10)"));
146 | // → {type: "apply",
147 | // operator: {type: "word", name: "+"},
148 | // args: [{type: "word", name: "a"},
149 | // {type: "value", value: 10}]}
150 | ```
151 |
152 | 程序可以正常工作了!当表达式解析失败时,解析函数不会输出任何有用的信息,也不会存储出错的行号与列号,而这些信息都有助于之后的错误报告。但考虑到我们的目的,这门语言目前已经足够优秀了。
153 |
154 | ## 求值器(evaluator)
155 |
156 | 在有了一个程序的语法树之后,我们该做什么呢?当然是执行程序了!而这就是求值器的功能。我们将语法树和作用域对象传递给求值器,执行器就会求解语法树中的表达式,然后返回整个过程的结果。
157 |
158 | ```js
159 | const specialForms = Object.create(null);
160 |
161 | function evaluate(expr, scope) {
162 | if (expr.type == "value") {
163 | return expr.value;
164 | } else if (expr.type == "word") {
165 | if (expr.name in scope) {
166 | return scope[expr.name];
167 | } else {
168 | throw new ReferenceError(
169 | `Undefined binding: ${expr.name}`);
170 | }
171 | } else if (expr.type == "apply") {
172 | let {operator, args} = expr;
173 | if (operator.type == "word" &&
174 | operator.name in specialForms) {
175 | return specialForms[operator.name](expr.args, scope);
176 | } else {
177 | let op = evaluate(operator, scope);
178 | if (typeof op == "function") {
179 | return op(...args.map(arg => evaluate(arg, scope)));
180 | } else {
181 | throw new TypeError("Applying a non-function.");
182 | }
183 | }
184 | }
185 | }
186 | ```
187 |
188 | 求值器为每一种表达式类型都提供了相应的处理逻辑。字面值表达式产生自身的值(例如,表达式`100`的求值为数值`100`)。对于绑定而言,我们必须检查程序中是否实际定义了该绑定,如果已经定义,则获取绑定的值。
189 |
190 | 应用则更为复杂。若应用有特殊形式(比如`if`),我们不会求解任何表达式,而是将表达式参数和环境传递给处理这种形式的函数。如果是普通调用,我们求解运算符,验证其是否是函数,并使用求值后的参数调用函数。
191 |
192 | 我们使用一般的 JavaScript 函数来表示 Egg 的函数。在定义特殊格式`fun`时,我们再回过头来看这个问题。
193 |
194 | `evaluate`的递归结构类似于解析器的结构。两者都反映了语言自身的结构。我们也可以将解析器和求值器集成到一起,在解析的同时求解表达式,但将其分离为两个阶段使得程序更易于理解。
195 |
196 | 这就是解释 Egg 所需的全部代码。这段代码非常简单,但如果不定义一些特殊的格式,或向环境中添加一些有用的值,你无法使用该语言完成很多工作。
197 |
198 | ## 特殊形式
199 |
200 | `specialForms`对象用于定义 Egg 中的特殊语法。该对象将单词和求解这种形式的函数关联起来。目前该对象为空,现在让我们添加`if`。
201 |
202 | ```js
203 | specialForms.if = (args, scope) => {
204 | if (args.length != 3) {
205 | throw new SyntaxError("Wrong number of args to if");
206 | } else if (evaluate(args[0], scope) !== false) {
207 | return evaluate(args[1], scope);
208 | } else {
209 | return evaluate(args[2], scope);
210 | }
211 | };
212 | ```
213 |
214 | Egg 的`if`语句需要三个参数。Egg 会求解第一个参数,若结果不是`false`,则求解第二个参数,否则求解第三个参数。相较于 JavaScript 中的`if`语句,Egg 的`if`形式更类似于 JavaScript 中的`?:`运算符。这是一条表达式,而非语句,它会产生一个值,即第二个或第三个参数的结果。
215 |
216 | Egg 和 JavaScript 在处理条件值时也有些差异。Egg 不会将 0 或空字符串作为假,只有当值确实为`false`时,测试结果才为假。
217 |
218 | 我们之所以需要将`if`表达为特殊形式,而非普通函数,是因为函数的所有参数需要在函数调用前求值完毕,而`if`则只应该根据第一个参数的值,确定求解第二个还是第三个参数。`while`的形式也是类似的。
219 |
220 | ```js
221 | specialForms.while = (args, scope) => {
222 | if (args.length != 2) {
223 | throw new SyntaxError("Wrong number of args to while");
224 | }
225 | while (evaluate(args[0], scope) !== false) {
226 | evaluate(args[1], scope);
227 | }
228 |
229 | // Since undefined does not exist in Egg, we return false,
230 | // for lack of a meaningful result.
231 | return false;
232 | };
233 | ```
234 |
235 | 另一个基本的积木是`do`,会自顶向下执行其所有参数。整个`do`表达式的值是最后一个参数的值。
236 |
237 | ```js
238 | specialForms.do = (args, scope) => {
239 | let value = false;
240 | for (let arg of args) {
241 | value = evaluate(arg, scope);
242 | }
243 | return value;
244 | };
245 | ```
246 |
247 | 我们还需要创建名为`define`的形式,来创建绑定对绑定赋值。`define`的第一个参数是一个单词,第二个参数是一个会产生值的表达式,并将第二个参数的计算结果赋值给第一个参数。由于`define`也是个表达式,因此必须返回一个值。我们则规定`define`应该将我们赋予绑定的值返回(就像 JavaScript 中的`=`运算符一样)。
248 |
249 | ```js
250 | specialForms.define = (args, scope) => {
251 | if (args.length != 2 || args[0].type != "word") {
252 | throw new SyntaxError("Incorrect use of define");
253 | }
254 | let value = evaluate(args[1], scope);
255 | scope[args[0].name] = value;
256 | return value;
257 | };
258 | ```
259 |
260 | ## 环境
261 |
262 | `evaluate`所接受的作用域是一个对象,它的名称对应绑定名称,它的值对应这些绑定所绑定的值。 我们定义一个对象来表示全局作用域。
263 |
264 | 我们需要先定义布尔绑定才能使用之前定义的`if`语句。由于只有两个布尔值,因此我们不需要为其定义特殊语法。我们简单地将`true`、`false`两个名称与其值绑定即可。
265 |
266 | ```js
267 | const topScope = Object.create(null);
268 |
269 | topScope.true = true;
270 | topScope.false = false;
271 | ```
272 |
273 | 我们现在可以求解一个简单的表达式来对布尔值求反。
274 |
275 | ```js
276 | let prog = parse(`if(true, false, true)`);
277 | console.log(evaluate(prog, topScope));
278 | // → false
279 | ```
280 |
281 | 为了提供基本的算术和比较运算符,我们也添加一些函数值到作用域中。为了确保代码短小,我们在循环中使用` Function`来合成一批运算符,而不是分别定义所有运算符。
282 |
283 | ```js
284 | for (let op of ["+", "-", "*", "/", "==", "<", ">"]) {
285 | topScope[op] = Function("a, b", `return a ${op} b;`);
286 | }
287 | ```
288 |
289 | 输出也是一个实用的功能,因此我们将`console.log`包装在一个函数中,并称之为`print`。
290 |
291 | ```js
292 | topScope.print = value => {
293 | console.log(value);
294 | return value;
295 | };
296 | ```
297 |
298 | 这样一来我们就有足够的基本工具来编写简单的程序了。下面的函数提供了一个便利的方式来编写并运行程序。它创建一个新的环境对象,并解析执行我们赋予它的单个程序。
299 |
300 | ```js
301 | function run(program) {
302 | return evaluate(parse(program), Object.create(topScope));
303 | }
304 | ```
305 |
306 | 我们将使用对象原型链来表示嵌套的作用域,以便程序可以在不改变顶级作用域的情况下,向其局部作用域添加绑定。
307 |
308 | ```js
309 | run(`
310 | do(define(total, 0),
311 | define(count, 1),
312 | while(<(count, 11),
313 | do(define(total, +(total, count)),
314 | define(count, +(count, 1)))),
315 | print(total))
316 | `);
317 | // → 55
318 | ```
319 |
320 | 我们之前已经多次看到过这个程序,该程序计算数字 1 到 10 的和,只不过这里使用 Egg 语言表达。很明显,相较于实现同样功能的 JavaScript 代码,这个程序并不优雅,但对于一个不足 150 行代码的程序来说已经很不错了。
321 |
322 | ## 函数
323 |
324 | 每个功能强大的编程语言都应该具有函数这个特性。
325 |
326 | 幸运的是我们可以很容易地添加一个`fun`语言构造,`fun`将最后一个参数当作函数体,将之前的所有名称用作函数参数。
327 |
328 | ```js
329 | specialForms.fun = (args, scope) => {
330 | if (!args.length) {
331 | throw new SyntaxError("Functions need a body");
332 | }
333 | let body = args[args.length - 1];
334 | let params = args.slice(0, args.length - 1).map(expr => {
335 | if (expr.type != "word") {
336 | throw new SyntaxError("Parameter names must be words");
337 | }
338 | return expr.name;
339 | });
340 |
341 | return function() {
342 | if (arguments.length != params.length) {
343 | throw new TypeError("Wrong number of arguments");
344 | }
345 | let localScope = Object.create(scope);
346 | for (let i = 0; i < arguments.length; i++) {
347 | localScope[params[i]] = arguments[i];
348 | }
349 | return evaluate(body, localScope);
350 | };
351 | };
352 | ```
353 |
354 | Egg 中的函数可以获得它们自己的局部作用域。 `fun`形式产生的函数创建这个局部作用域,并将参数绑定添加到它。 然后求解此范围内的函数体并返回结果。
355 |
356 | ```js
357 | run(`
358 | do(define(plusOne, fun(a, +(a, 1))),
359 | print(plusOne(10)))
360 | `);
361 | // → 11
362 |
363 | run(`
364 | do(define(pow, fun(base, exp,
365 | if(==(exp, 0),
366 | 1,
367 | *(base, pow(base, -(exp, 1)))))),
368 | print(pow(2, 10)))
369 | `);
370 | // → 1024
371 | ```
372 |
373 | ## 编译
374 |
375 | 我们构建的是一个解释器。在求值期间,解释器直接作用域由解析器产生的程序的表示。
376 |
377 | 编译是在解析和运行程序之间添加的另一个步骤:通过事先完成尽可能多的工作,将程序转换成一些可以高效求值的东西。例如,在设计良好的编程语言中,使用每个绑定时绑定引用的内存地址都是明确的,而不需要在程序运行时进行动态计算。这样可以省去每次访问绑定时搜索绑定的时间,只需要直接去预先定义好的内存位置获取绑定即可。
378 |
379 | 一般情况下,编译会将程序转换成机器码(计算机处理可以执行的原始格式)。但一些将程序转换成不同表现形式的过程也被认为是编译。
380 |
381 | 我们可以为 Egg 编写一个可供选择的求值策略,首先使用`Function`,调用 JavaScript 编译器编译代码,将 Egg 程序转换成 JavaScript 程序,接着执行编译结果。若能正确实现该功能,可以使得 Egg 运行的非常快,而且实现这种编译器确实非常简单。
382 |
383 | 如果读者对该话题感兴趣,愿意花费一些时间在这上面,建议你尝试实现一个编译器作为练习。
384 |
385 | ## 站在别人的肩膀上
386 |
387 | 我们定义`if`和`while`的时候,你可能注意到他们封装得或多或少和 JavaScript 自身的`if`、`while`有点像。同样的,Egg 中的值也就是 JavaScript 中的值。
388 |
389 | 如果读者比较一下两种 Egg 的实现方式,一种是基于 JavaScrip t之上,另一种是直接使用机器提供的功能构建程序设计语言,会发现第二种方案需要大量工作才能完成,而且非常复杂。不管怎么说,本章的内容就是想让读者对编程语言的运行方式有一个基本的了解。
390 |
391 | 当需要完成一些任务时,相比于自己完成所有工作,借助于别人提供的功能是一种更高效的方式。虽然在本章中我们编写的语言就像玩具一样,十分简单,而且无论在什么情况下这门语言都无法与 JavaScript 相提并论。但在某些应用场景中,编写一门微型语言可以帮助我们更好地完成工作。
392 |
393 | 这些语言不需要像传统的程序设计语言。例如,若 JavaScript 没有正则表达式,你可以为正则表达式编写自己的解析器和求值器。
394 |
395 | 或者想象一下你在构建一个巨大的机械恐龙,需要编程实现恐龙的行为。JavaScript 可能不是实现该功能的最高效方式,你可以选择一种语言作为替代,如下所示:
396 |
397 | ```egg
398 | behavior walk
399 | perform when
400 | destination ahead
401 | actions
402 | move left-foot
403 | move right-foot
404 |
405 | behavior attack
406 | perform when
407 | Godzilla in-view
408 | actions
409 | fire laser-eyes
410 | launch arm-rockets
411 | ```
412 |
413 | 这通常被称为领域特定语言(Domain-specific Language),一种为表达极为有限的知识领域而量身定制的语言。它可以准确描述其领域中需要表达的事物,而没有多余元素。这种语言比通用语言更具表现力。
414 |
415 | ## 习题
416 |
417 | ### 数组
418 |
419 | 在 Egg 中支持数组需要将以下三个函数添加到顶级作用域:`array(...values)`用于构造一个包含参数值的数组,`length(array)`用于获取数组长度,`element(array, n)`用于获取数组中的第`n`个元素。
420 |
421 | ```js
422 | // Modify these definitions...
423 |
424 | topScope.array = "...";
425 |
426 | topScope.length = "...";
427 |
428 | topScope.element = "...";
429 |
430 | run(`
431 | do(define(sum, fun(array,
432 | do(define(i, 0),
433 | define(sum, 0),
434 | while(<(i, length(array)),
435 | do(define(sum, +(sum, element(array, i))),
436 | define(i, +(i, 1)))),
437 | sum))),
438 | print(sum(array(1, 2, 3))))
439 | `);
440 | // → 6
441 | ```
442 |
443 |
444 | ### 闭包
445 |
446 | 我们定义`fun`的方式允许函数引用其周围环境,就像 JavaScript 函数一样,函数体可以使用在定义该函数时可以访问的所有局部绑定。
447 |
448 | 下面的程序展示了该特性:函数f返回一个函数,该函数将其参数和f的参数相加,这意味着为了使用绑定a,该函数需要能够访问f中的局部作用域。
449 |
450 | ```js
451 | run(`
452 | do(define(f, fun(a, fun(b, +(a, b)))),
453 | print(f(4)(5)))
454 | `);
455 | // → 9
456 | ```
457 |
458 | 回顾一下fun形式的定义,解释一下该机制的工作原理。
459 |
460 | ### 注释
461 |
462 | 如果我们可以在 Egg 中编写注释就太好了。例如,无论何时,只要出现了井号(`#`),我们都将该行剩余部分当成注释,并忽略之,就类似于 JavaScript 中的`//`。
463 |
464 | 解析器并不需要为支持该特性进行大幅修改。我们只需要修改`skipSpace`来像跳过空白符号一样跳过注释即可,此时调用`skipSpace`时不仅会跳过空白符号,还会跳过注释。修改代码,实现这样的功能。
465 |
466 | ```js
467 | // This is the old skipSpace. Modify it...
468 | function skipSpace(string) {
469 | let first = string.search(/\S/);
470 | if (first == -1) return "";
471 | return string.slice(first);
472 | }
473 |
474 | console.log(parse("# hello\nx"));
475 | // → {type: "word", name: "x"}
476 |
477 | console.log(parse("a # one\n # two\n()"));
478 | // → {type: "apply",
479 | // operator: {type: "word", name: "a"},
480 | // args: []}
481 | ```
482 |
483 |
484 | ### 修复作用域
485 |
486 | 目前绑定赋值的唯一方法是`define`。该语言构造可以同时实现定义绑定和将一个新的值赋予已存在的绑定。
487 |
488 | 这种歧义性引发了一个问题。当你尝试为一个非局部绑定赋予新值时,你最后会定义一个局部绑定并替换掉原来的同名绑定。一些语言的工作方式正和这种设计一样,但是我总是认为这是一种笨拙的作用域处理方式。
489 |
490 | 添加一个类似于`define`的特殊形式`set`,该语句会赋予一个绑定新值,若绑定不存在于内部作用域,则更新其外部作用域相应绑定的值。若绑定没有定义,则抛出`ReferenceError`(另一个标准错误类型)。
491 |
492 | 我们目前采取的技术是使用简单的对象来表示作用域对象,处理目前的任务非常方便,此时我们需要更进一步。你可以使用`Object.getPrototypeOf`函数来获取对象原型。同时也要记住,我们的作用域对象并未继承`Object.prototype`,因此若想调用`hasOwnProperty`,需要使用下面这个略显复杂的表达式。
493 |
494 | ```js
495 | Object.prototype.hasOwnProperty.call(scope, name);
496 | ```
497 |
498 | ```js
499 | specialForms.set = (args, scope) => {
500 | // Your code here.
501 | };
502 |
503 | run(`
504 | do(define(x, 4),
505 | define(setx, fun(val, set(x, val))),
506 | setx(50),
507 | print(x))
508 | `);
509 | // → 50
510 | run(`set(quux, true)`);
511 | // → Some kind of ReferenceError
512 | ```
513 |
--------------------------------------------------------------------------------
/13.md:
--------------------------------------------------------------------------------
1 | ## 十三、浏览器中的 JavaScript
2 |
3 | > 原文:[JavaScript and the Browser](https://eloquentjavascript.net/13_browser.html)
4 | >
5 | > 译者:[飞龙](https://github.com/wizardforcel)
6 | >
7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
8 | >
9 | > 自豪地采用[谷歌翻译](https://translate.google.cn/)
10 | >
11 | > 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
12 |
13 | > Web 背后的梦想是公共信息空间,其中我们通过共享信息进行交流。 其普遍性至关重要:超文本链接可指向任何东西,无论是个人的,本地的还是全球的,无论是草稿还是高度润色的。
14 | >
15 | > Douglas Crockford,《JavaScript 编程语言》(视频讲座)
16 |
17 | 
18 |
19 | 本书接下来的章节将会介绍 Web 浏览器。可以说,没有浏览器,就没有 JavaScript。就算有,估计也不会有多少人去关心这门编程语言。
20 |
21 | Web 技术自出现伊始,其演变方式和技术上就是以分散的方式发展的。许多浏览器厂商专门为其开发新的功能,有时这些新功能被大众采纳,有时这些功能被其他功能所代替,最终形成了一套标准。
22 |
23 | 这种发展模式是把双刃剑。一方面,不会有一个集中式的组织来管理技术的演进,取而代之的是一个包含多方利益集团的松散协作架构(偶尔会出现对立)。另一方面,互联网这种无计划的发展方式所开发出来的系统,其内部很难实现一致性。事实上,它的一些部分令人疑惑,并且毫无设计。
24 |
25 | ## 网络和 Internet
26 |
27 | 计算机网络出现在 20 世纪 50 年代。如果在两台或多台计算机之间铺设电缆,那么你可以通过这些电缆互相收发数据,并实现一些神奇的功能。
28 |
29 | 如果通过连接同一个建筑中的两台机器就可以实现一些神奇的功能,那么如果可以连接全世界的机器,就可以完成更伟大的工作了。20 世纪 80 年代,人们开发了相关技术来实现这个愿景,我们将其产生的网络称为 Internet。而 Internet 的表现名副其实。
30 |
31 | 计算机可以使用这种网络向其他计算机发送位数据。为了在传输位数据的基础上,实现计算机之间的有效通信,网络两端的机器必须知道这些位所表达的实际含义。对于给定的位序列,其含义完全取决于位序列描述的信息类型与使用的编码机制。
32 |
33 | 网络协议描述了一种网络通信方式。网络协议非常多,其中包括邮件发送、邮件收取和邮件共享,甚至连病毒软件感染控制计算机都有相应的协议。
34 |
35 | 例如,HTTP(超文本传输协议,Hypertext Transfer Protocol)是用于检索命名资源(信息块,如网页或图片)的协议。 它指定发出请求的一方应该以这样的一行开始,命名资源和它正在尝试使用的协议的版本。
36 |
37 | ```
38 | GET /index.html HTTP/1.1
39 | ```
40 |
41 | 有很多规则,关于请求者在请求中包含更多信息的方式,以及另一方返回资源并打包其内容的方式。 我们将在第 18 章中更详细地观察 HTTP。
42 |
43 | 大多数协议都建立在其他协议之上。 HTTP 将网络视为一种流式设备,您可以将位放入这些设备,并使其按正确的顺序到达正确的目的地。 我们在第 11 章]中看到,确保这些事情已经是一个相当困难的问题。
44 |
45 |
46 | TCP(传输控制协议,Transmission Control Protocol)就可以帮助我们解决该问题。所有连接到互联网的设备都会使用到这种协议,而多数互联网通信都构建在这种协议之上。
47 |
48 | TCP 连接的工作方式是一台电脑必须等待或者监听,而另一台电脑则开始与之通信。一台机器为了同时监听不同类型的通信信息,会为每个监听器分配一个与之关联的数字(我们称之为端口)。大多数协议都指定了默认使用的端口。例如,当我们向使用 SMTP 协议发送一封邮件时,我们需要通过一台机器来发送邮件,而发送邮件的机器需要监听端口 25。
49 |
50 | 随后另一台机器连接到使用了正确端口号的目标机器上。如果可以连接到目标机器,而且目标机器在监听对应端口,则说明连接创建成功。负责监听的计算机名为服务器,而连接服务器的计算机名为客户端。
51 |
52 | 我们可以将该连接看成双向管道,位可以在其中流动,也就是说两端的机器都可以向连接中写入数据。当成功传输完这些位数据后,双方都可以读取另一端传来的数据。TCP 是一个非常便利的模型。我们可以说TCP就是一种网络的抽象。
53 |
54 | ## Web
55 |
56 | 万维网(World Wide Web,不要将其与 Internet 混淆)是包含一系列协议和格式的集合,允许我们通过浏览器访问网页。词组中的 Web 指的是这些页面可以轻松地链接其他网页,因此最后可以连接成一张巨大的网,用户可以在网络中浏览。
57 |
58 | 你只需将一台计算机连接到 Internet 并使用 HTTP 监听 80 端口,就可以成为 Web 的一部分。其他计算机可以通过网络,并使用 HTTP 协议获取其他计算机上的文件。
59 |
60 | 网络中的每个文件都能通过 URL(统一资源定位符,Universal Resource Locator)访问,如下所示:
61 |
62 | ```
63 | http://eloquentjavascript.net/13_browser.html
64 | | | | |
65 | protocol server path
66 | ```
67 | 该地址的第一部分告诉我们 URL 使用的是 HTTP 协议(加密的 HTTP 连接则使用`https://`来表示)。第二部分指的是获取文件的服务器地址。第三部分是我们想要获取的具体文件(或资源)的路径。
68 |
69 | 连接到互联网的机器获得一个 IP 地址,该地址是一个数字,可用于将消息发送到该机器的,类似于`"149.210.142.219"`或`"2001:4860:4860::8888"`。 但是或多或少的随机数字列表很难记住,而且输入起来很笨拙,所以你可以为一个特定的地址或一组地址注册一个域名。 我注册了`eloquentjavascript.net`,来指向我控制的机器的 IP 地址,因此可以使用该域名来提供网页。
70 |
71 | 如果你在浏览器地址栏中输入上面提到的 URL,浏览器会尝试获取并显示该 URL 对应的文档。首先,你的浏览器需要找出域名`eloquentjavascript.net`指向的地址。然后使用 HTTP 协议,连接到该地址处的服务器,并请求`/13_browser.html`这个资源。如果一切顺利,服务器会发回一个文档,然后您的浏览器将显示在屏幕上。
72 |
73 | ## HTML
74 |
75 | HTML,即超文本标记语言(Hypertext Markup Language),是在网页中得到广泛使用的文档格式。HTML 文档不仅包含文本,还包含了标签,用于说明文本结构,描述了诸如链接、段落、标题之类的元素。
76 |
77 | 一个简短的 HTML 文档如下所示:
78 |
79 | ```html
80 |
81 |
82 |
83 |
84 | My home page
85 |
86 |
87 | My home page
88 | Hello, I am Marijn and this is my home page.
89 | I also wrote a book! Read it
90 | here.
91 |
92 |
93 | ```
94 |
95 | 标签包裹在尖括号之间(`<`和`>`,小于和大于号),提供关于文档结构的信息。其他文本则是纯文本。
96 |
97 | 文档以``开头,告诉浏览器将页面解释为现代 HTML,以别于过去使用的各种方言。
98 |
99 | HTML 文档有头部(head)和主体(body)。头部包含了文档信息,而主体则包含文档自身。在本例中,头部将文档标题声明为`"My home page"`,并使用 UTF-8 编码,它是将 Unicode 文本编码为二进制的方式。文档的主体包含标题(``,表示一级标题,``到``可以产生不同等级的子标题)和两个段落(`
`)。
100 |
101 | 标签有几种形式。一个元素,比如主体、段落或链接以一个起始标签(比如`
`)开始,并以一个闭合标签(比如`
`)结束。一些起始标签,比如一个链接(``),会包含一些额外信息,其形式是`name="value"`这种键值对,我们称之为属性。在本例中,使用属性`href="http://eloquentjavascript.net"`指定链接的目标,其中`href`表示“超文本链接(Hypertext Reference)”。
102 |
103 | 某些类型的标签不会包含任何元素,这种标签不需要闭合。元数据标签``就是一个例子。
104 |
105 | > 译者注:最好还是这样闭合它们:``。
106 |
107 | 尽管 HTML 中尖括号有特殊含义,但为了在文档的文本中包含这些符号,可以引入另外一种形式的特殊标记方法。普通文本中的起始尖括号写成`<`(less than),而闭合尖括号写成`>`(greater than)。在 HTML 中,我们将一个`&`字符后跟着一个单词和分号(`;`)这种写法称为一个实体,浏览器会使用实体编码对应的字符替换它们。
108 |
109 | 与之类似的是 JavaScript 字符串中反斜杠的使用。由于 HTML 中的实体机制赋予了`&`特殊含义,因此我们需要使用`&`来表示一个`&`字符。在属性的值(包在双引号中)中使用`"`可以插入实际的引号字符。
110 |
111 | HTML 的解析过程容错性非常强。当应有的标签丢失时,浏览器会重新构建这些标签。标签的重新构建已经标准化,你可以认为所有现代浏览器的行为都是一致的。
112 |
113 | 下面的文件与之前版本显示效果相同:
114 |
115 | ```html
116 |
117 |
118 |
119 | My home page
120 |
121 | My home page
122 | Hello, I am Marijn and this is my home page.
123 |
I also wrote a book! Read it
124 | here.
125 | ```
126 |
127 | ``、`
`和``标签可以完全丢弃。浏览器知道``和``属于头部,而``属于主体。此外,我再也不用明确关闭某个段落,因为新段落开始或文档结束时,浏览器会隐式关闭段落标签。目标链接两边的引号也可以丢弃。
128 |
129 | 本书的示例通常都会省略``、``和``标签,以保持源代码简短,避免太过杂乱。但我会明确关闭所有标签并在属性两旁包含引号。
130 |
131 | 本书也会经常忽略`doctype`和`charset`声明。这并不是鼓励大家省略它们。当你忘记它们时,浏览器往往会做出荒谬的事情。 您应该认为`doctype`和`charset`元数据隐式出现在示例中,即使它们没有实际显示在文本中。
132 |
133 | ## HTML 和 JavaScript
134 |
135 | 对于本书来说,最重要的一个 HTML 标签是`
140 | ```
141 |
142 | 当浏览器在读取 HTML 时,一旦遇到`
149 | ```
150 |
151 | 这里包含的文件`code/hello.js`是和上文中相同的一段程序,`alert("hello")`。当一个页面将其他 URL 引用为自身的一部分时(比如图像文件或脚本),网页浏览器将会立即获取这些资源并将其包含在页面中。
152 |
153 | 即使`script`标签引用了一个文本文件,且并未包含任何代码,你也必须使用``来闭合标签。如果你忘记了这点,浏览器会将剩余的页面会作为脚本的一部分进行解析。
154 |
155 | 你可以在浏览器中加载ES模块(参见第 10 章),向脚本标签提供`type ="module"`属性。 这些模块可以依赖于其他模块,通过将相对于自己的 URL 用作`import`声明中的模块名称。
156 |
157 |
158 | 某些属性也可以包含 JavaScript 程序。下面展示的`