零、前言
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.
Ellen Ullman,《Close to the Machine: Technophilia and its Discontents》
这是一本关于指导电脑的书。时至今日,计算机就像螺丝刀一样随处可见,但相比于螺丝刀而言,计算机更复杂一些,并且,让他们做你想让他们做的事情,并不总是那么容易。
如果你让计算机执行的任务是常见的,易于理解的任务,例如向你显示你的电子邮件,或像计算器一样工作,则可以打开相应的应用并开始工作。 但对于独特的或开放式的任务,应用可能不存在。
这就是编程可能出现的地方。编程是构建一个程序的行为 - 它是一组精确的指令,告诉计算机做什么。 由于计算机是愚蠢的,迂腐的野兽,编程从根本上是乏味和令人沮丧的。
幸运的是,如果你可以克服这个事实,并且甚至可以享受愚蠢机器可以处理的严谨思维,那么编程可以是非常有益的。 它可以让你在几秒钟内完成手动操作。 这是一种方法,让你的电脑工具去做它以前做不到的事情。 它也提供了抽象思维的优秀练习。
大多数编程都是用编程语言完成的。 编程语言是一种人工构建的语言,用于指导计算机。 有趣的是,我们发现与电脑沟通的最有效的方式,与我们彼此沟通的方式相差太大。 与人类语言一样,计算机语言可以以新的方式组合词语和词组,从而可以表达新的概念。
在某种程度上,基于语言的界面,例如 80 年代和 90 年代的 BASIC 和 DOS 提示符,是与计算机交互的主要方法。 这些已经在很大程度上被视觉界面取代,这些视觉界面更容易学习,但提供更少的自由。 计算机语言仍然存在,如果你知道在哪里看到。 每种现代 Web 浏览器都内置了一种这样的语言,即 JavaScript,因此几乎可以在所有设备上使用。
本书将试图让你足够了解这门语言,从而完成有用和有趣的东西。
关于程序设计
除了讲解 JavaScript 之外,本书也会介绍一些程序设计的基本原则。程序设计还是比较复杂的。编程的基本规则简单清晰,但在这些基本规则之上构建的程序却容易变得复杂,导致程序产生了自己的规则和复杂性。即便程序是按照你自己的思路去构建的,你也有可能迷失在代码之间。
在阅读本书时,你有可能会觉得书中的概念难以理解。如果你刚刚开始学习编程,那么你估计还有不少东西需要掌握呢。如果你想将所学知识融会贯通,那么就需要去多参考和学习一些资料。
是否付出必要的努力完全取决于你自己。当你阅读本书的时候发现任何难点,千万不要轻易就对自己的能力下结论。只要能坚持下去,你就是好样的。稍做休息,复习一下所学的知识点,始终确保自己阅读并理解了示例程序和相关的练习。学习是一项艰巨的任务,但你掌握的所有知识都属于你自己,而且今后的学习道路会愈加轻松。
当行动无利可图时,就收集信息;当信息无利可图时,就休息。
Ursula K. Le Guin,《The Left Hand of Darkness》
一个程序有很多含义:它是开发人员编写的一段文本、计算机执行的一段指令集合、计算机内存当中的数据以及控制内存中数据的操作集合。我们通常很难将程序与我们日常生活中熟悉的事物进行对比。有一种表面上比较恰当的比喻,即将程序视作包含许多组件的机器,为了让机器正常工作,这些组件通过内部通信来实现整个机器的正常运转。
计算机是一台物理机器,充当这些非物质机器的载体。计算机本身并不能实现多么复杂的功能,但计算机之所以有用是因为它们的运算速度非常快。而程序的作用就是将这些看似简单的动作组合起来,然后实现复杂的功能。
程序是思想的结晶。编写程序不需要什么物质投入,它很轻量级,通过我们的双手创造。
但如果不稍加注意,程序的体积和复杂度就会失去控制,甚至代码的编写者也会感到迷惑。在可控的范围内编写程序是编程过程中首要解决的问题。当程序运行时,一切都是那么美好。编程的精粹就在于如何更好地控制复杂度。质量高的程序的复杂度都不会太高。
很多开发人员认为,控制程序复杂度的最好方法就是避免使用不熟悉的技术。他们制定了严格的规则(“最佳实践”),并小心翼翼地呆在他们安全区内。
这不仅无聊,而且也是无效的。新问题往往需要新的解决方案。编程领域还很年轻,仍然在迅速发展,并且多样到足以为各种不同的方法留出空间。在程序设计中有许多可怕的错误,你应该继续犯错,以便你能理解它们。好的程序看起来是什么样的感觉,是在实践中发展的,而不是从一系列规则中学到的。
为什么编程语言重要
在计算技术发展伊始,并没有编程语言这个概念。程序看起来就像这样:
00110001 00000000 00000000
00110001 00000001 00000001
00110011 00000001 00000010
01010001 00001011 00000010
00100010 00000010 00001000
01000011 00000001 00000000
01000001 00000001 00000001
00010000 00000010 00000000
01100010 00000000 00000000
该程序计算数字 1~10 之和,并打印出结果:1+2+...+10=55
。该程序可以运行在一个简单的机器上。在早期计算机上编程时,我们需要在正确的位置设置大量开关阵列,或在纸带上穿孔并将纸带输入计算机中。你可以想象这个过程是多么冗长乏味且易于出错。即便是编写非常简单的程序,也需要有经验的人耗费很大精力才能完成。编写复杂的程序则更是难上加难。
当然了,手动输入这些晦涩难懂的位序列(1 和 0)来编写程序的确能让程序员感到很有成就感,而且能给你的职业带来极大的满足感。
在上面的程序中,每行都包含一条指令。我们可以用中文来描述这些指令:
将数字 0 存储在内存地址中的位置 0。
将数字 1 存储在内存地址的位置 1。
将内存地址的位置 1 中的值存储在内存地址的位置 2。
将内存地址的位置 2 中的值减去数字 11。
如果内存地址的位置 2 中的值是 0,则跳转到指令 9。
将内存地址的位置 1 中的值加到内存地址的位置 0。
将内存地址的位置 1 中的值加上数字 1。
跳转到指令 3。
输出内存地址的位置 0 中的值。
虽说这已经比一大堆位序列要好读了许多,但仍然不清晰。使用名称而不是数字用于指令和存储位置有所帮助:
Set “total” to 0.
Set “count” to 1.
[loop]
Set “compare” to “count”.
Subtract 11 from “compare”.
If “compare” is zero, continue at [end].
Add “count” to “total”.
Add 1 to “count”.
Continue at [loop].
[end]
Output “total”.
现在你能看出该程序是如何工作的吗?前两行代码初始化两个内存位置的值:total
用于保存累加计算结果,而count
则用于记录当前数字。你可能觉得compare
的那行代码看起来有些奇怪。程序想根据count
是否等于 11 来决定是否应该停止运行。因为我们的机器相当原始,所以只能测试一个数字是否为 0,并根据它做出决策。因此程序用名为compare
的内存位置存放count–11
的值,并根据该值是否为 0 决定是否跳转。接下来两行将count
的值累加到结果上,并将count
加 1,直到count
等于11
为止。
下面使用 JavaScript 重新编写了上面的程序:
let total = 0, count = 1;
while (count <= 10) {
total += count;
count += 1;
}
console.log(total);
// → 55
这个版本的程序得到了一些改进。更为重要的是,我们再也不需要指定程序如何来回跳转了,而是由while
结构负责完成这个任务。只要我们给予的条件成立,while
语句就会不停地执行其下方的语句块(包裹在大括号中)。而我们给予的条件是count<=10
,意思是“count
小于等于 10”。我们再也不需要创建临时的值并将其与 0 比较,那样的代码十分烦琐。编程语言的一项职责就是,能够帮助我们处理这些烦琐无趣的逻辑。
在程序的结尾,也就是while
语句结束后,我们使用console.log
操作来输出结果。
最后,我们恰好有range
和sum
这类方便的操作。下面代码中的range
函数用于创建数字集合,sum
函数用于计算数字集合之和:
console.log(sum(range(1, 10)));
// → 55
我们可以从这里了解到,同一个程序的长度可长可短,可读性可高可低。第一个版本的程序晦涩难懂,而最后一个版本的程序则接近于人类语言的表达方式:将 1~10 范围内的数字之和记录下来(我们会在后面的章节中详细介绍如何编写sum
和range
这样的函数)。
优秀的编程语言可以为开发人员提供更高层次的抽象,使用类似于人类语言的方式来与计算机进行交互。它有助于省略细节,提供便捷的积木(比如while
和console.log
),允许你定义自己的积木(比如sum
和range
函数),并使这些积木易于编写。。
什么是 JavaScript
JavaScript 诞生于 1995 年。起初,Netscape Navigator 浏览器将其运用在网页上添加程序。自此以后,各类主流图形网页浏览器均采用了 JavaScript。JavaScript 使得现代网页应用程序成为可能 —— 使用 JavaScript 可以直接与用户交互,从而避免每一个动作都需要重新载入页面。但有许多传统网站也会使用 JavaScript 来提供实时交互以及更加智能的表单功能。
JavaScript 其实和名为Java的程序设计语言没有任何关系。起了这么一个相似的名字完全是市场考虑使然,这并非是一个明智的决定。当 JavaScript 出现时,Java 语言已在市场上得到大力推广且拥有了极高人气,因此某些人觉得依附于 Java 的成功是个不错的主意。而我们现在已经无法摆脱这个名字了。
在 JavaScript 被广泛采用之后,ECMA 国际制订了一份标准文档来描述 JavaScript 的工作行为,以便所有声称支持 JavaScript 的软件都使用同一种语言。标准化完成后,该标准被称为 ECMAScript 标准。实际上,术语 ECMAScript 和 JavaScript 可以交换使用。它们不过是同一种语言的两个名字而已。
许多人会说 JavaScript 语言的坏话。这其中有很多这样的言论都是正确的。当被要求第一次使用 JavaScript 编写代码时,我当时就觉得这门语言难以驾驭。JavaScript 接受我输入的任何代码,但是又使用和我的想法完全不同的方式来解释代码。由于我没有任何线索知道我之前做了什么,因此我需要做出更多工作,但这也就存在一个实际问题:我们可以自由使用 JavaScript,而这种自由却几乎没有限度。这种设计其实是希望初学者更容易使用 JavaScript 编写程序。但实际上,系统不会指出我们错在何处,因此从程序中找出问题变得更加棘手。
但这种自由性也有其优势,许多技术在更为严格的语言中不可能实现,而在 JavaScript 中则留下了实现的余地,正如你看到的那样(比如第十章),有些优势可以弥补 JavaScript 的一些缺点。在正确地学习 JavaScript 并使用它工作了一段时间后,我真正喜欢上了 JavaScript。
JavaScript 版本众多。大约在 2000~2010 年间,这正是 JavaScript 飞速发展的时期,浏览器支持最多的是 ECMAScript 3。在此期间,ECMA 着手制定 ECMAScript 4,这是一个雄心勃勃的版本,ECMA 计划在这个版本中加入许多彻底的改进与扩展。但由于 ECMAScript 3 被广泛使用,这种过于激进的修改必然会遭遇重重阻碍,最后 ECMA 不得不于 2008 年放弃了版本 4 的制定。这就产生了不那么雄心勃勃的版本 5,这只是一些没有争议的改进,出现在 2009 年。 然后版本 6 在 2015 年诞生,这是一个重大的更新,其中包括计划用于版本 4 的一些想法。从那以后,每年都会有新的更新。
语言不断发展的事实意味着,浏览器必须不断跟上,如果你使用的是较老的浏览器,它可能不支持每个特性。 语言设计师会注意,不要做任何可能破坏现有程序的改变,所以新的浏览器仍然可以运行旧的程序。 在本书中,我使用的是 2017 版的 JavaScript。
Web 浏览器并不是唯一一个可以运行 JavaScript 的平台。有些数据库,比如 MongoDB 和 CouchDB,也使用 JavaScript 作为脚本语言和查询语言。一些桌面和服务器开发的平台,特别是 Node.js 项目(第二十章介绍),为浏览器以外的 JavaScript 编程提供了一个环境。
代码及相关工作
代码是程序的文本内容。本书多数章节都介绍了大量代码。我相信阅读代码和编写代码是学习编程不可或缺的部分。尝试不要仅仅看一眼示例,而应该认真阅读并理解每个示例。刚开始使用这种方式可能会速度较慢并为代码所困惑,但我坚信你很快就可以熟能生巧。对待习题的方法也应该一样。除非你确实已经编写代码解决了问题,否则不要假设你已经理解了问题。
建议读者应尝试在实际的 JavaScript 解释器中执行习题代码。这样一来,你就可以马上获知代码工作情况的反馈,而且我希望读者去做更多的试验,而不仅仅局限于习题的要求。
可以在 http://eloquentjavascript.net/ 中查阅本书的在线版本,并运行和实验本书中的代码。也可以在在线版本中点击任何代码示例来编辑、运行并查看其产生的输出。在做习题时,你可以访问 http://eloquentjavascript.net/code/,该网址会提供每个习题的初始代码,让你专心于解答习题。
如果想要在本书提供的沙箱以外执行本书代码,需要稍加注意。许多的示例是独立的,而且可以在任何 JavaScript 环境下运行。但后续章节的代码大多数都是为特定环境(浏览器或者 Node.js)编写的,而且只能在这些特定环境下执行代码。此外,许多章节定义了更大的程序,这些章节中出现的代码片段会互相依赖或是依赖于一些外部文件。本书网站的沙箱提供了 zip 压缩文件的链接,该文件包含了所有运行特定章节代码所需的脚本和数据文件。
本书概览
本书包括三个部分。前十二章讨论 JavaScript 语言本身的一些特性。接下来的 8 章讨论网页浏览器和 JavaScript 在网页编程中的实践。最后两章专门讲解另一个使用 JavaScript 编程的环境 —— Node.js。
纵观本书,共有 5 个项目实战章,用于讲解规模较大的示例程序,你可以通过这些章来仔细品味真实的编程过程。根据项目出现次序,我们会陆续构建递送机器人(7)、程序设计语言(12)、平台游戏(16)、像素绘图程序(19)和一个动态网站(21)。
本书介绍编程语言时,首先使用4章来介绍 JavaScript 语言的基本结构,包括第二章控制结构(比如在本前言中看到的while
单词)、第三章函数(编写你自己的积木)和第四章数据结构。此后你就可以编写简单的程序了。接下来,第五章和第六章介绍函数和对象的运用技术,以编写更加抽象的代码并以此来控制复杂度。
介绍完第一个项目实战(7)之后,将会继续讲解语言部分,例如第八章错误处理和 bug 修复、第九章正则表达式(处理文本数据的重要工具)、第十章模块化(解决复杂度的问题)以及第十一章异步编程(处理需要时间的事件)。第二个项目实战章节(12)则是对本书第一部分的总结。
第二部分(第十三章到第十九章),阐述了浏览器 JavaScript 中的一些工具。你将会学到在屏幕上显示某些元素的方法(第十四章与第十七章),响应用户输入的方法(第十五章)和通过网络通信的方法(第十八章)。这部分又有两个项目实战章节。
此后,第二十章阐述 Node.js,而第二十一章使用该工具构建一个简单的网页系统。
本书版式约定
本书中存在大量代码,程序(包括你迄今为止看到的一些示例)代码的字体如下所示:
function factorial(n) {
if (n == 0) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
为了展示程序产生的输出,本书常在代码后编写代码期望输出,输出结果前会加上两个反斜杠和一个箭头。
console.log(factorial(8));
// → 40320
祝好运!