简介

我们认为我们是在为了自己的目的创造系统。我们相信我们是在按照自己的形象创造它…… 但是计算机并不像我们。它是我们自身非常狭隘一部分的投射:致力于逻辑、秩序、规则和清晰的那部分。

埃伦·乌尔曼,《接近机器:技术狂热及其不满》
Illustration of a screwdriver next to a circuit board of about the same size

这是一本关于指导计算机的书籍。如今,计算机和螺丝刀一样常见,但它们要复杂得多,让它们做你想让它们做的事情并不总是容易的。

如果你对计算机的任务是常见的、众所周知的,比如向你展示你的电子邮件或充当计算器,你可以打开相应的应用程序并开始工作。但对于独特或开放式任务,通常没有合适的应用程序。

这就是编程可能发挥作用的地方。编程是指构建程序的行为,程序是一组精确的指令,告诉计算机该怎么做。因为计算机是愚蠢的、死板的野兽,所以编程从根本上来说是乏味和令人沮丧的。

幸运的是,如果你能克服这一事实——甚至可能享受用愚蠢的机器可以理解的方式思考的严谨性——编程会很有回报。它允许你在几秒钟内完成手工完成需要很长时间的事情。它是一种让你的计算机工具做以前无法做的事情的方式。最重要的是,它为解决难题和抽象思维提供了一个绝妙的游戏。

大多数编程都是用编程语言完成的。编程语言是一种人工构建的语言,用于指导计算机。有趣的是,我们发现与计算机交流最有效的方式很大程度上借鉴了我们彼此交流的方式。就像人类语言一样,计算机语言允许将单词和短语以新的方式组合起来,从而能够表达出不断涌现的新概念。

在某个时候,基于语言的界面,例如 1980 年代和 1990 年代的 BASIC 和 DOS 提示符,是与计算机交互的主要方法。对于常规的计算机使用,这些界面已被视觉界面取代,视觉界面更容易学习,但提供的自由度更低。但如果你知道在哪里寻找,这些语言仍然存在。其中之一,JavaScript,内置于每个现代 Web 浏览器中——因此在几乎所有设备上都可用。

本书将尝试让你熟悉这种语言,以便用它做一些有用和有趣的事情。

关于编程

除了解释 JavaScript,我还会介绍编程的基本原理。事实证明,编程很难。基本规则简单明了,但构建在这些规则之上的程序往往会变得足够复杂,以至于会引入自己的规则和复杂性。从某种程度上来说,你正在构建自己的迷宫,你很容易迷失其中。

有时阅读本书会让人感到非常沮丧。如果你不熟悉编程,将会有很多新内容要消化。其中许多内容随后会以需要你进行额外关联的方式组合起来。

你要做出必要的努力。当你难以理解本书的内容时,不要对自己的能力妄下结论。你很好——你只需要坚持下去。休息一下,重新阅读一些资料,并确保你阅读并理解示例程序和练习。学习是艰苦的工作,但你学到的所有东西都是你的,并将使你更容易地继续学习。

当行动变得无利可图时,就收集信息;当信息变得无利可图时,就睡觉。

厄休拉·K·勒古恩,《黑暗的左手》

程序是多种事物。它是程序员键入的一段文本,它是使计算机执行其操作的指导力量,它是计算机内存中的数据,同时它也控制着对该内存执行的操作。试图将程序与熟悉物体进行比较的类比往往达不到预期效果。一个表面上合适的类比是将程序比作机器——往往会涉及许多独立的部分,为了使整个机器运转起来,我们必须考虑这些部分相互连接并对整个机器的操作做出贡献的方式。

计算机是一台物理机器,作为这些非物质机器的宿主。计算机本身只能做愚蠢的、直接的事情。它们之所以如此有用,是因为它们以极高的速度执行这些操作。一个程序可以巧妙地组合大量这些简单操作来完成非常复杂的事情。

程序是思想的建筑。建造它不花成本,它没有重量,它在我们打字的手下很容易生长。但随着程序的增长,它的复杂性也随之增加。编程的技巧是构建不会让程序员感到困惑的程序的技巧。最好的程序是那些能够做一些有趣的事情,同时又易于理解的程序。

一些程序员认为,这种复杂性最好通过在他们的程序中只使用一组经过充分理解的技术来管理。他们制定了严格的规则(“最佳实践”)来规定程序应该具有的形式,并仔细地停留在他们安全的小区域内。

这不仅很无聊——而且它效率低下。新问题往往需要新的解决方案。编程领域还很年轻,并且仍在快速发展,它足够多样化,可以容纳各种不同的方法。在程序设计中,有很多可怕的错误要犯,你应该至少犯一次,这样你才能了解它们。对优秀程序外貌的理解是通过实践培养出来的,而不是从规则列表中学习的。

语言为何重要

在开始的时候,在计算的诞生之初,没有编程语言。程序看起来像这样

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)确实让程序员有一种成为强大巫师的深刻感觉。在工作满意度方面,这肯定有其价值。

前面程序的每一行都包含一条指令。它可以用英语这样写

  1. 将数字 0 存储在内存位置 0 中。

  2. 将数字 1 存储在内存位置 1 中。

  3. 将内存位置 1 的值存储在内存位置 2 中。

  4. 从内存位置 2 的值中减去数字 11。

  5. 如果内存位置 2 的值为数字 0,则继续执行指令 9。

  6. 将内存位置 1 的值添加到内存位置 0 中。

  7. 将数字 1 添加到内存位置 1 的值中。

  8. 继续执行指令 3。

  9. 输出内存位置 0 的值。

尽管这已经比位汤更易读,但它仍然相当晦涩难懂。使用名称而不是数字来表示指令和内存位置会有所帮助。

  Set “total” to 0.
  Set “count” to 1.
[loop]
  Set “compare” to “count”.
  Subtract 11 from “compare”.
  If “compare” is 0, continue at [end].
  Add “count” to “total”.
  Add 1 to “count”.
  Continue at [loop].
[end]
  Output “total”.

你能看到程序是如何工作的吗?前两行给两个内存位置赋予了初始值:total 将用于构建计算结果,count 将用于跟踪我们当前查看的数字。使用 compare 的行可能是最令人困惑的。程序想要查看 count 是否等于 11,以决定它是否可以停止运行。因为我们的假想机器非常原始,它只能测试一个数字是否为零,并根据该值做出决策。因此,它使用标记为 compare 的内存位置来计算 count - 11 的值,并根据该值做出决策。接下来的两行在程序判定 count 还不等于 11 时,将 count 的值添加到结果中,并将 count 增加 1。

以下是该程序在 JavaScript 中的写法

let total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);
// → 55

这个版本为我们提供了更多改进。最重要的是,不再需要指定我们希望程序如何来回跳转——while 结构会处理这个问题。只要给定的条件成立,它就会继续执行它下面的块(用大括号括起来)。该条件是 count <= 10,表示“count 小于或等于 10”。我们不再需要创建一个临时值并将其与零进行比较,这仅仅是一个无关紧要的细节。编程语言的力量之一在于它们可以为我们处理无关紧要的细节。

在程序结束时,while 结构完成后,console.log 操作用于输出结果。

最后,如果我们碰巧可以使用 rangesum 操作,这两个操作分别创建一定范围内的数字集合和计算数字集合的总和,那么程序可以看起来像这样

console.log(sum(range(1, 10)));
// → 55

这个故事的寓意是,同一个程序可以用长短不一的、不可读和可读的方式来表达。程序的第一个版本非常晦涩,而最后一个版本几乎是英语:log 1 到 10 之间数字的sum 。(我们将在后面的章节中看到如何定义像sumrange 这样的操作。)

好的编程语言通过允许程序员在更高层面上讨论计算机必须执行的动作来帮助程序员。它有助于省略细节,提供方便的构建块(如whileconsole.log),允许你定义自己的构建块(如sumrange),并使这些块易于组合。

什么是 JavaScript?

JavaScript 在 1995 年被引入,作为一种将程序添加到 Netscape Navigator 浏览器网页中的方式。此后,该语言已被所有其他主要的图形 Web 浏览器采用。它使现代 Web 应用程序成为可能——也就是说,你可以直接与之交互的应用程序,而无需在每次操作时都进行页面重新加载。JavaScript 也用于更传统的网站,以提供各种交互性和巧妙性。

需要注意的是,JavaScript 与名为 Java 的编程语言几乎没有任何关系。类似的名称是出于营销考虑而不是明智的判断而得来的。当 JavaScript 被引入时,Java 语言正在被大力推广,并正在流行起来。有人认为搭乘这股成功浪潮是个好主意。现在我们被这个名字困住了。

在它在 Netscape 之外被采用后,一份标准文档被编写出来描述 JavaScript 语言的工作方式,以便宣称支持 JavaScript 的各种软件能够确保它们实际上提供了相同的语言。这被称为 ECMAScript 标准,以进行标准化的 Ecma 国际组织命名。在实践中,ECMAScript 和 JavaScript 术语可以互换使用——它们是同一种语言的两个名称。

有些人会对 JavaScript 说可怕的话。这些话中的很多都是真的。当我第一次被要求用 JavaScript 写点东西时,我很快就开始讨厌它。它几乎会接受我输入的任何东西,但会以一种与我的本意完全不同的方式来解释它。当然,这与我当时完全不知道自己在做什么有很大关系,但这里确实存在一个问题:JavaScript 在允许的范围内非常宽松。这种设计的初衷是让初学者更容易用 JavaScript 编程。实际上,它主要使查找程序中的问题变得更加困难,因为系统不会向你指出它们。

不过,这种灵活性也有它的优势。它为在更严格的语言中不可能实现的技术留下了空间,并营造了一种令人愉快的非正式编程风格。在正确学习了这门语言并在一段时间内使用它之后,我开始真正地喜欢 JavaScript。

JavaScript 已经有了几个版本。ECMAScript 版本 3 是 JavaScript 崛起过程中得到广泛支持的版本,大约在 2000 年到 2010 年之间。在此期间,人们一直在努力开发一个雄心勃勃的版本 4,它计划对语言进行一系列激进的改进和扩展。以这样一种激进的方式改变一种活生生的、被广泛使用的语言,事实证明在政治上是困难的,版本 4 的工作于 2008 年被放弃。一个野心远不如版本 4 的版本 5 于 2009 年问世,它只进行了一些无争议的改进。2015 年,版本 6 问世,这是一个重大更新,其中包含了为版本 4 计划的一些想法。从那时起,我们每年都会有新的、小的更新。

JavaScript 不断发展的事实意味着浏览器必须不断跟上。如果你使用的是旧浏览器,它可能不支持所有功能。语言设计人员很谨慎地避免进行任何可能破坏现有程序的更改,因此新浏览器仍然可以运行旧程序。在这本书中,我使用的是 2024 年版的 JavaScript。

Web 浏览器并不是 JavaScript 唯一使用的平台。一些数据库,例如 MongoDB 和 CouchDB,使用 JavaScript 作为它们的脚本和查询语言。几个用于桌面和服务器编程的平台,最著名的是 Node.js 项目(第 20 章的主题),提供了在浏览器之外编程 JavaScript 的环境。

代码,以及如何处理代码

代码是构成程序的文本。本书中的大多数章节都包含相当多的代码。我认为阅读代码和编写代码是学习编程不可或缺的一部分。不要只是扫一眼这些例子——认真阅读并理解它们。这在开始时可能很慢很混乱,但我保证你很快就会掌握它。练习也是如此。不要以为自己理解了它们,除非你已经真正写出了一个可行的解决方案。

我建议你在实际的 JavaScript 解释器中尝试你的练习解决方案。这样,你就可以立即得到关于你正在做的事情是否有效的反馈,而且我希望你会被诱惑去尝试并超越练习。

当你在浏览器中阅读本书时,你可以通过点击来编辑(并运行)所有示例程序。

在本书网站之外运行本书中定义的程序需要小心。许多示例是独立的,应该在任何 JavaScript 环境中都能运行。但后面的章节中的代码通常是为特定环境(浏览器或 Node.js)编写的,只能在那里运行。此外,许多章节定义了更大的程序,它们中出现的代码片段相互依赖,或者依赖于外部文件。网站上的沙盒提供了指向 ZIP 文件的链接,这些文件包含运行特定章节的代码所需的所有脚本和数据文件。

本书概述

本书大致包含三个部分。前 12 章讨论 JavaScript 语言。接下来的七章是关于 Web 浏览器以及如何使用 JavaScript 对它们进行编程的。最后,两章专门介绍 Node.js,它是另一个用于编程 JavaScript 的环境。本书中共有五个项目章节,描述了更大的示例程序,让你体验真实的编程。

本书的语言部分从四章开始,介绍 JavaScript 语言的基本结构。它们讨论了控制结构(例如你在本介绍中看到的while 词)、函数(编写你自己的构建块)和数据结构。学习完这些内容后,你就可以编写基本的程序了。接下来,第5 章和第6 章介绍了使用函数和对象来编写更抽象的代码以及控制复杂性的技术。

第一个项目章节构建了一个粗糙的送货机器人之后,本书的语言部分继续介绍了有关错误处理和调试正则表达式(处理文本的重要工具)、模块化(另一种抵御复杂性的方法)和异步编程(处理需要时间的事件)的章节。本书第一部分的最后一部分是第二个项目章节,我们将在其中实现一门编程语言。

本书的第二部分,第13 章至第19 章,描述了浏览器 JavaScript 可以访问的工具。你将学习如何在屏幕上显示内容(第14 章和第17 章)、响应用户输入(第15 章)以及通过网络进行通信(第18 章)。这部分中又有两个项目章节:构建一个平台游戏和一个像素绘画程序.

第 20 章描述了 Node.js,而第 21 章则使用该工具构建了一个小型网站。

排版约定

在这本书中,用等宽字体书写的文本将代表程序的元素。有时它们是自包含的片段,有时它们只是指代附近程序的一部分。程序(你已经见过一些)的编写方式如下

function factorial(n) {
  if (n == 0) {
    return 1;
  } else {
    return factorial(n - 1) * n;
  }
}

有时,为了显示程序产生的输出,预期的输出将在程序之后写出,前面有两个斜杠和一个箭头。

console.log(factorial(8));
// → 40320

祝你好运!