第二章程序结构
我的心在薄薄的、半透明的皮肤下发出鲜红的亮光,他们不得不给我注射 10 毫升的 JavaScript 才能让我恢复过来。(我对血液中的毒素反应良好。)天哪,那东西会把你的腮帮子都踢出来!
在本章中,我们将开始做一些真正可以被称为编程的事情。我们将扩展对 JavaScript 语言的掌握,从我们目前为止看到的名词和句子片段,到能够表达一些有意义的散文。
表达式和语句
在第一章中,我们创建了一些值,然后将运算符应用于它们以获得新值。创建这样的值是每个 JavaScript 程序的必要部分,但这只是一部分。
产生值的代码片段称为表达式。直接写出的每个值(例如 22
或 "psychoanalysis"
)都是一个表达式。括号内的表达式也是表达式,将二元运算符应用于两个表达式或将一元运算符应用于一个表达式也是如此。
这展示了基于语言的接口部分的美丽之处。表达式的嵌套方式与人类语言中子句的嵌套方式非常相似——一个子句可以包含自己的子句,依此类推。这使我们能够组合表达式来表达任意复杂的计算。
如果表达式对应于句子片段,则 JavaScript语句对应于人类语言中的完整句子。程序仅仅是一个语句列表。
1; !false;
不过,这是一个无用的程序。表达式可以仅仅产生一个值,该值随后可以被包含的表达式使用。语句独立存在,只有当它影响世界时才有意义。它可以显示屏幕上的内容——这算得上是改变世界——或者它可以以影响后续语句的方式改变机器的内部状态。这些变化称为副作用。前一个示例中的语句只是产生值 1
和 true
,然后立即将其丢弃。这不会对世界造成任何影响。在执行程序时,不会发生任何可观察到的事件。
在某些情况下,JavaScript 允许您省略语句末尾的分号。在其他情况下,它必须存在,否则下一行将被视为同一语句的一部分。安全省略它的规则有些复杂且容易出错。在这本书中,每个需要分号的语句都将始终以分号结尾。我建议你在自己的程序中也这样做,至少在你了解更多关于省略分号的微妙之处之前。
变量
程序如何保持内部状态?它如何记住事物?我们已经了解了如何从旧值中产生新值,但这不会改变旧值,新值必须立即使用,否则它将再次消散。为了捕捉和保存值,JavaScript 提供了一个名为变量的东西。
var caught = 5 * 5;
这给了我们第二种语句。特殊词语(关键字)var
指示此句子将定义一个变量。它后面跟着变量的名称,如果我们想立即为它赋予一个值,则后面跟着一个 =
运算符和一个表达式。
前面的语句创建了一个名为 caught
的变量,并使用它来抓住通过将 5 乘以 5 生成的数字。
在定义变量后,可以使用其名称作为表达式。此表达式的值为变量当前保存的值。以下是一个示例
var ten = 10; console.log(ten * ten); // → 100
变量名称可以是任何不是保留字的单词(例如 var
)。它们不能包含空格。数字也可以是变量名称的一部分——例如 catch22
是一个有效的名称——但名称不能以数字开头。变量名称不能包含标点符号,除了字符 $
和 _
。
当变量指向某个值时,并不意味着它永远绑定到该值。=
运算符可以随时对现有变量使用,以将其与当前值断开连接,并使其指向一个新值。
var mood = "light"; console.log(mood); // → light mood = "dark"; console.log(mood); // → dark
你应该将变量想象成触手,而不是盒子。它们不包含值;它们抓住它们——两个变量可以引用同一个值。程序只能访问它仍然持有的值。当您需要记住某些东西时,您需要伸出触手抓住它,或者将您现有的触手之一重新连接到它。
让我们来看一个例子。为了记住路易吉欠你的美元数,你需要创建一个变量。然后,当他偿还 35 美元时,你就会给这个变量一个新的值。
var luigisDebt = 140; luigisDebt = luigisDebt - 35; console.log(luigisDebt); // → 105
当您在没有为变量赋值的情况下定义它时,触手就无法抓住任何东西,所以它会悬空。如果您询问空变量的值,您将获得值 undefined
。
var one = 1, two = 2; console.log(one + two); // → 3
关键字和保留字
具有特殊含义的词语,例如 var
,是关键字,它们不能用作变量名称。还有一些词语“保留用于”未来版本的 JavaScript。这些词语也不允许用作变量名称,尽管一些 JavaScript 环境允许它们。关键字和保留字的完整列表相当长。
break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield
不用担心记住这些,但请记住,当变量定义无法按预期工作时,这可能是问题所在。
环境
在给定时间存在的变量及其值的集合称为环境。当程序启动时,此环境不是空的。它始终包含作为语言标准一部分的变量,并且大多数情况下,它具有提供与周围系统交互方式的变量。例如,在浏览器中,存在检查和影响当前加载的网站以及读取鼠标和键盘输入的变量和函数。
函数
默认环境中提供的许多值都具有函数类型。函数是封装在值中的程序片段。这些值可以应用以运行封装的程序。例如,在浏览器环境中,变量 alert
包含一个函数,该函数会显示一个小对话框,其中包含一条消息。它的使用方法如下
alert("Good morning!");
执行函数称为调用、调用或应用它。您可以通过在产生函数值的表达式后加上括号来调用函数。通常,您会直接使用保存函数的变量的名称。括号之间的值将传递给函数内的程序。在示例中,alert
函数使用我们传递给它的字符串作为在对话框中显示的文本。传递给函数的值称为参数。alert
函数只需要其中一个,但其他函数可能需要不同数量或不同类型的参数。
console.log 函数
alert
函数在进行实验时可以作为输出设备使用,但一直点击所有这些小窗口会让你心烦。在前面的示例中,我们使用 console.log
来输出值。大多数 JavaScript 系统(包括所有现代 Web 浏览器和 Node.js)提供了一个 console.log
函数,该函数会将其参数写入某些文本输出设备。在浏览器中,输出会出现在 JavaScript 控制台中。浏览器的这部分界面默认情况下是隐藏的,但大多数浏览器会在您按下 F12 或在 Mac 上按下 Command-Option-I 时打开它。如果这不起作用,请在菜单中搜索名为“Web 控制台”或“开发者工具”的项目。
在运行示例或您自己的代码时,在本书页面上,console.log
输出将显示在示例之后,而不是在浏览器的 JavaScript 控制台中。
var x = 30; console.log("the value of x is", x); // → the value of x is 30
虽然变量名不能包含句点字符,但 console.log
显然包含一个。这是因为 console.log
不是一个简单的变量。它实际上是一个表达式,它从 console
变量持有的值中检索 log
属性。我们将在第四章中详细了解这意味着什么。
返回值
显示对话框或将文本写入屏幕是副作用。许多函数因为它们产生的副作用而有用。函数也可以产生值,在这种情况下,它们不需要有副作用才能有用。例如,函数 Math.max
接受任意数量的数字值并返回最大的值。
console.log(Math.max(2, 4)); // → 4
当函数产生一个值时,它被称为返回该值。在 JavaScript 中,任何产生值的都是表达式,这意味着函数调用可以在更大的表达式中使用。这里对 Math.min
的调用(与 Math.max
相反)用作加号运算符的输入
console.log(Math.min(2, 4) + 100); // → 102
下一章将解释如何编写自己的函数。
prompt 和 confirm
浏览器环境除了 alert
之外还包含其他用于弹出窗口的函数。您可以使用 confirm
向用户询问一个确认/取消问题。它将返回一个布尔值:如果用户点击确认,则返回 true
,如果用户点击取消,则返回 false
。
confirm("Shall we, then?");
prompt
函数可用于询问一个“开放式”问题。第一个参数是问题,第二个参数是用户开始使用的文本。可以在对话框窗口中键入一行文本,函数将返回该文本作为字符串。
prompt("Tell me everything you know.", "...");
这两个函数在现代 Web 编程中很少使用,主要是因为您无法控制生成的窗口的外观,但它们对于玩具程序和实验很有用。
控制流
当您的程序包含多个语句时,这些语句将按顺序从上到下执行。作为一个基本的示例,此程序包含两个语句。第一个语句询问用户一个数字,第二个语句(随后执行)显示该数字的平方。
var theNumber = Number(prompt("Pick a number", "")); alert("Your number is the square root of " + theNumber * theNumber);
Number
函数将值转换为数字。我们需要这种转换,因为 prompt
的结果是字符串值,而我们想要一个数字。还有类似的函数称为 String
和 Boolean
,它们将值转换为这些类型。
条件执行
按直线顺序执行语句不是我们唯一的选择。另一种方法是条件执行,我们根据布尔值在两条不同的路径之间进行选择,如下所示
条件执行在 JavaScript 中用 if
关键字编写。在简单情况下,我们只希望在特定条件成立时执行一些代码。例如,在前面的程序中,我们可能希望仅当输入实际上是数字时才显示输入的平方。
var theNumber = Number(prompt("Pick a number", "")); if (!isNaN(theNumber)) alert("Your number is the square root of " + theNumber * theNumber);
使用此修改,如果您输入“cheese”,则不会显示任何输出。
if
关键字根据布尔表达式的值来执行或跳过语句。决定性表达式写在关键字之后,用括号括起来,后面跟着要执行的语句。
isNaN
函数是一个标准的 JavaScript 函数,仅当它给出的参数是 NaN
时才返回 true
。Number
函数在您给它一个不代表有效数字的字符串时恰好返回 NaN
。因此,该条件翻译为“除非 theNumber
不是数字,否则执行此操作”。
您通常不会只编写在条件成立时执行的代码,还会编写处理其他情况的代码。此备用路径由图中的第二条箭头表示。else
关键字可以与 if
一起使用来创建两条独立的备用执行路径。
var theNumber = Number(prompt("Pick a number", "")); if (!isNaN(theNumber)) alert("Your number is the square root of " + theNumber * theNumber); else alert("Hey. Why didn't you give me a number?");
如果我们有多于两条路径可以选择,则可以将多个 if
/else
对“链接”在一起。以下是一个示例
var num = Number(prompt("Pick a number", "0")); if (num < 10) alert("Small"); else if (num < 100) alert("Medium"); else alert("Large");
该程序首先检查 num
是否小于 10。如果是,则选择该分支,显示 "Small"
,然后完成。如果不是,则选择 else
分支,该分支本身包含第二个 if
。如果第二个条件 (< 100
) 成立,则意味着该数字介于 10 和 100 之间,并且显示 "Medium"
。如果不是,则选择第二个也是最后一个 else
分支。
while 和 do 循环
考虑一个打印从 0 到 12 的所有偶数的程序。一种方法是如下所示
console.log(0); console.log(2); console.log(4); console.log(6); console.log(8); console.log(10); console.log(12);
这有效,但编写程序的目的是减少工作量,而不是增加工作量。如果我们需要所有小于 1,000 的偶数,则之前的程序将无法使用。我们需要的是一种重复某些代码的方法。这种形式的控制流称为循环
循环控制流使我们能够回到程序中的某个之前的位置并重复它,并使用当前的程序状态。如果将此与计数变量结合使用,我们可以执行以下操作
var number = 0; while (number <= 12) { console.log(number); number = number + 2; } // → 0 // → 2 // … etcetera
以 while
关键字开头的语句创建一个循环。while
之后是一个括号中的表达式,然后是一个语句,非常类似于 if
。只要表达式生成一个转换为布尔类型时为 true
的值,循环就会执行该语句。
在这个循环中,我们想要打印当前数字并为我们的变量加 2。每当我们需要在循环内执行多个语句时,我们都会将它们用花括号 ({
和 }
) 括起来。花括号对语句的作用与括号对表达式的作用相同:它们将它们组合在一起,使它们作为一个语句。用花括号括起来的语句序列称为块。
许多 JavaScript 程序员将每个循环或 if
体都用花括号括起来。他们这样做是为了保持一致性,并在以后更改主体中的语句数量时避免添加或删除花括号。在这本书中,我将编写大多数不带花括号的单语句体,因为我重视简洁。您可以选择您喜欢的任何风格。
变量 number
演示了变量可以跟踪程序进度的过程。每次循环重复时,number
都增加 2
。然后,在每次重复开始时,它将与数字 12
进行比较,以确定程序是否完成了它打算做的一切工作。
作为一个实际上做了一些有用事情的例子,我们现在可以编写一个程序来计算并显示 210(2 的 10 次方)的值。我们使用两个变量:一个用于跟踪我们的结果,另一个用于计算我们已经将此结果乘以 2 的次数。循环测试第二个变量是否已经达到 10,然后更新这两个变量。
var result = 1; var counter = 0; while (counter < 10) { result = result * 2; counter = counter + 1; } console.log(result); // → 1024
计数器也可以从 1
开始,并检查 <= 10
,但由于在 第 4 章 中将要说明的原因,最好习惯从 0 开始计数。
do
循环是一种类似于 while
循环的控制结构。它只在一方面有所不同:do
循环始终至少执行一次其主体,并且它只在第一次执行后才开始测试它是否应该停止。为了反映这一点,测试出现在循环主体之后
do { var yourName = prompt("Who are you?"); } while (!yourName); console.log(yourName);
此程序将强制您输入姓名。它将反复询问,直到获得非空字符串。应用 !
运算符将在否定之前将值转换为布尔类型,除了 ""
之外的所有字符串都将转换为 true
。这意味着循环将继续循环,直到您提供一个非空字符串的名称。
缩进代码
您可能已经注意到我在某些语句前面加的空格。在 JavaScript 中,这些不是必需的——即使没有它们,计算机也能正常接受程序。事实上,程序中的换行符也是可选的。如果您愿意,可以将程序写成一行。块内缩进的作用是使代码的结构突出。在复杂的代码中,如果在新块在其他块内打开,则可能难以看到一个块的结束位置和另一个块的开始位置。使用适当的缩进,程序的视觉形状将对应于其内部块的形状。我喜欢每个打开的块使用两个空格,但口味不同——有些人使用四个空格,有些人使用制表符。
for 循环
许多循环都遵循之前 while
示例中看到的模式。首先,创建一个“计数器”变量来跟踪循环的进度。然后是一个 while
循环,其测试表达式通常检查计数器是否已经达到某个边界。在循环主体结束时,更新计数器以跟踪进度。
由于这种模式非常常见,因此 JavaScript 和类似语言提供了一种稍微更短、更全面的形式,即 for
循环。
for (var number = 0; number <= 12; number = number + 2) console.log(number); // → 0 // → 2 // … etcetera
此程序与 前面的 偶数打印示例完全等效。唯一的变化是所有与循环“状态”相关的语句现在都分组在一起。
for
关键字后面的括号必须包含两个分号。第一个分号之前的部分初始化循环,通常是通过定义一个变量。第二部分是用于检查循环是否必须继续的表达式。最后一部分在每次迭代后更新循环的状态。在大多数情况下,这比 while
结构更短、更清晰。
以下是用 for
而不是 while
来计算 210 的代码
var result = 1; for (var counter = 0; counter < 10; counter = counter + 1) result = result * 2; console.log(result); // → 1024
请注意,即使没有用 {
打开块,循环中的语句仍然缩进了两个空格,以明确表明它“属于”它前面的行。
退出循环
使循环的条件生成 false
不是循环结束的唯一方式。有一个特殊的语句称为 break
,它具有立即跳出封闭循环的效果。
此程序说明了 break
语句。它找到第一个大于或等于 20 且可被 7 整除的数字。
for (var current = 20; ; current++) { if (current % 7 == 0) break; } console.log(current); // → 21
使用余数 (%
) 运算符是测试一个数字是否可以被另一个数字整除的简单方法。如果是,则它们的除法的余数为零。
示例中的 for
结构没有用于检查循环结束的部分。这意味着除非执行内部的 break
语句,否则循环永远不会停止。
如果您要省略该 break
语句,或者不小心写了一个始终生成 true
的条件,那么您的程序将陷入无限循环。陷入无限循环的程序永远不会完成运行,这通常是一件坏事。
如果您在这些页面上的示例之一中创建了一个无限循环,通常会问您是否要在几秒钟后停止脚本。如果失败,您将不得不关闭您正在使用的选项卡,或在某些浏览器中关闭整个浏览器,才能恢复。
continue
关键字类似于 break
,因为它会影响循环的进度。当在循环主体中遇到 continue
时,控制将跳出主体并继续执行循环的下一轮迭代。
简洁地更新变量
尤其是在循环时,程序通常需要“更新”一个变量,以根据该变量的先前值保存一个值。
counter = counter + 1;
counter += 1;
类似的快捷方式适用于许多其他运算符,例如 result *= 2
用于将 result
翻倍,或 counter -= 1
用于向下计数。
for (var number = 0; number <= 12; number += 2) console.log(number);
对于 counter += 1
和 counter -= 1
,还有更短的等效项:counter++
和 counter--
。
使用 switch 进行值分发
if (variable == "value1") action1(); else if (variable == "value2") action2(); else if (variable == "value3") action3(); else defaultAction();
有一个名为 switch
的结构,旨在以更直接的方式解决这种“分发”。不幸的是,JavaScript 用于此的语法(它继承自 C/Java 编程语言系列)有点笨拙——一连串的 if
语句通常看起来更好。以下是一个例子
switch (prompt("What is the weather like?")) { case "rainy": console.log("Remember to bring an umbrella."); break; case "sunny": console.log("Dress lightly."); case "cloudy": console.log("Go outside."); break; default: console.log("Unknown weather type!"); break; }
您可以在 switch
打开的块中放置任意数量的 case
标签。程序将跳转到与 switch
接收的值相对应的标签,或者如果找不到匹配的值,则跳转到 default
。它从那里开始执行语句,即使它们在另一个标签下,直到它遇到 break
语句。在某些情况下,例如示例中的 "sunny"
案例,这可用于在案例之间共享一些代码(它建议在阳光明媚和阴天都外出)。但请注意:很容易忘记这样的 break
,这将导致程序执行您不希望执行的代码。
大写
变量名不能包含空格,但使用多个单词来清楚地描述变量表示的内容通常很有帮助。这些是您用几个单词编写变量名的选择
fuzzylittleturtle fuzzy_little_turtle FuzzyLittleTurtle fuzzyLittleTurtle
第一个风格可能难以阅读。就我个人而言,我喜欢下划线的样式,尽管这种样式打起来有点痛苦。标准的 JavaScript 函数以及大多数 JavaScript 程序员都遵循底部的样式——除了第一个单词之外,他们会将每个单词的首字母大写。习惯这种小事并不难,而代码混合命名风格会让人难以阅读,因此我们将遵循这种约定。
在一些情况下,例如 Number
函数,变量的第一个字母也大写。这样做是为了将此函数标记为构造函数。构造函数是什么将在 第 6 章 中说明。现在,重要的是不要被这种明显的缺乏一致性所困扰。
注释
通常,原始代码无法传达您希望程序传达给人类读者的所有信息,或者它以一种难以理解的方式传达信息,以至于人们可能无法理解它。在其他时候,您可能只是感到诗意或想要将一些想法作为程序的一部分包含在内。这就是注释的作用。
注释是程序的一部分文本,但计算机完全忽略它。JavaScript 有两种编写注释的方式。要编写单行注释,可以使用两个斜杠字符 (//
),然后是后面的注释文本。
var accountBalance = calculateBalance(account); // It's a green hollow where a river sings accountBalance.adjust(); // Madly catching white tatters in the grass. var report = new Report(); // Where the sun on the proud mountain rings: addToReport(accountBalance, report); // It's a little valley, foaming like light in a glass.
一个 //
注释只到行尾。/*
和 */
之间的文本部分将被忽略,无论它是否包含换行符。这通常用于添加有关文件或程序块的信息块。
/* I first found this number scrawled on the back of one of my notebooks a few years ago. Since then, it has often dropped by, showing up in phone numbers and the serial numbers of products that I've bought. It obviously likes me, so I've decided to keep it. */ var myNumber = 11213;
总结
您现在知道程序是由语句构建的,这些语句本身有时包含更多语句。语句往往包含表达式,这些表达式本身可以由更小的表达式构建。
将语句一个接一个地放置,您将获得一个从上到下执行的程序。您可以通过使用条件 (if
、else
和 switch
) 和循环 (while
、do
和 for
) 语句来引入控制流的干扰。
变量可用于在名称下存放数据片段,它们对于跟踪程序中的状态很有用。环境是定义的变量集合。JavaScript 系统总是将一些有用的标准变量放入您的环境中。
函数是封装程序片段的特殊值。您可以通过编写 functionName(argument1, argument2)
来调用它们。这种函数调用是一个表达式,并且可能会产生一个值。
练习
如果您不确定如何尝试解决练习的解决方案,请参考 介绍。
每个练习都从问题描述开始。阅读它并尝试解决练习。如果您遇到问题,请考虑阅读练习后的提示。练习的完整解决方案不包含在本手册中,但您可以在 eloquentjavascript.net/code 上在线找到它们。如果您想从练习中学习一些东西,我建议您只有在解决完练习后,或者至少在您长时间努力解决练习,并感到头痛时,才查看解决方案。
循环三角形
编写一个循环,该循环对 console.log
进行七次调用,以输出以下三角形
# ## ### #### ##### ###### #######
知道您可以通过在字符串后面编写 .length
来查找字符串的长度可能会有所帮助。
var abc = "abc"; console.log(abc.length); // → 3
大多数练习包含您可以修改以解决练习的代码片段。请记住,您可以单击代码块来编辑它们。
// Your code here.
您可以从一个简单地打印出数字 1 到 7 的程序开始,您可以通过对本章前面介绍的 偶数打印示例 进行一些修改来实现,其中介绍了 for
循环。
现在考虑数字和井号字符串之间的等价性。您可以通过添加 1 (+= 1
) 从 1 到 2。您可以通过添加一个字符 (+= "#"
) 从 "#"
到 "##"
。因此,您的解决方案可以紧密跟随数字打印程序。
FizzBuzz
编写一个程序,使用 console.log
打印从 1 到 100 的所有数字,但有两个例外。对于能被 3 整除的数字,打印 "Fizz"
而不是数字,对于能被 5 整除(但不能被 3 整除)的数字,打印 "Buzz"
而不是数字。
当您完成这些操作后,修改您的程序以打印 "FizzBuzz"
,用于能被 3 和 5 整除的数字(并且仍然打印 "Fizz"
或 "Buzz"
用于仅能被其中一个数字整除的数字)。
(这实际上是一个面试问题,据称可以淘汰相当一部分程序员候选人。所以,如果您解决了它,您现在可以对自己感到满意。)
// Your code here.
棋盘
编写一个程序,创建一个表示 8×8 网格的字符串,使用换行符来分隔行。网格的每个位置都有一个空格或一个“#”字符。这些字符应形成一个棋盘。
将此字符串传递给 console.log
应该显示类似以下内容
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
当您有一个生成这种模式的程序时,定义一个变量 size = 8
并在任何 size
上更改程序,输出具有给定宽度和高度的网格。
// Your code here.