程序结构
我的心脏在我薄薄的半透明皮肤下发出鲜红的亮光,他们必须给我注射 10 毫升的 JavaScript 才能让我恢复过来。(我对血液中的毒素反应良好。)我的天,那玩意会让你从腮帮子里吐出桃子!
在本章中,我们将开始做一些真正可以称为编程的事情。我们将扩展对 JavaScript 语言的掌握,超越我们迄今为止所看到的名词和句子片段,最终能够表达有意义的散文。
表达式和语句
在 第 1 章 中,我们创建了值,并对它们应用运算符以获得新的值。像这样创建值是任何 JavaScript 程序的主要实质。但是,这种实质必须在一个更大的结构中才能发挥作用。这正是我们将在本章中介绍的内容。
产生值的代码片段称为表达式。每个直接写出的值(例如 22
或 "psychoanalysis"
)都是一个表达式。括号内的表达式也是一个表达式,二元运算符作用于两个表达式或一元运算符作用于一个表达式也是如此。
这展示了基于语言的界面的部分美丽。表达式可以包含其他表达式,类似于人类语言中子句的嵌套方式——子句可以包含自己的子句,依此类推。这使我们能够构建表达式来描述任意复杂的计算。
如果表达式对应于句子片段,则 JavaScript 语句对应于完整句子。程序是语句的列表。
1;
!false;
不过,这是一个无用的程序。表达式可以满足于仅仅产生一个值,然后该值可以被包含它的代码使用。然而,语句是独立存在的,因此如果它不影响外部世界,它就毫无用处。它可能会在屏幕上显示一些东西,就像 console.log
一样,或者以一种会影响之后语句的方式改变机器的状态。这些变化被称为副作用。前面的示例中的语句只是产生值 1
和 true
,然后立即将其丢弃。这根本没有对外部世界留下任何印象。当你运行这个程序时,没有任何可观察到的现象发生。
在某些情况下,JavaScript 允许你在语句末尾省略分号。在其他情况下,它必须存在,否则下一行将被视为同一语句的一部分。安全省略它的规则有些复杂并且容易出错。因此,在这本书中,每个需要分号的语句都将始终带有分号。我建议你也这样做,至少在你了解更多关于省略分号的细微之处之前。
绑定
程序如何保持内部状态?它如何记住东西?我们已经看到如何从旧值产生新值,但这不会改变旧值,而且新值必须立即使用,否则它将再次消失。为了捕获和保存值,JavaScript 提供了一个名为绑定或变量的东西。
let caught = 5 * 5;
这给了我们第二种语句类型。特殊词(关键字)let
表示这句话将要定义一个绑定。它后面跟着绑定名称,如果我们想立即给它一个值,则后面跟着一个 =
运算符和一个表达式。
这个示例创建一个名为 caught
的绑定,并使用它来抓住 5 乘以 5 产生的数字。
定义绑定后,其名称可以用作表达式。此表达式的值为绑定当前保存的值。这是一个示例
let ten = 10; console.log(ten * ten); // → 100
当一个绑定指向一个值时,这并不意味着它永远绑定到那个值。=
运算符可以随时用于现有绑定,使其断开与当前值的连接并使其指向一个新的值
let mood = "light"; console.log(mood); // → light mood = "dark"; console.log(mood); // → dark
你应该将绑定想象成触手而不是盒子。它们并不包含值;它们抓住它们——两个绑定可以引用同一个值。程序只能访问它仍然具有引用的值。当你需要记住某件事时,你要么长出一条触手抓住它,要么重新连接你现有的触手之一到它。
让我们看另一个例子。为了记住路易吉还欠你的美元数,你创建了一个绑定。当他偿还 35 美元时,你给这个绑定一个新的值。
let luigisDebt = 140; luigisDebt = luigisDebt - 35; console.log(luigisDebt); // → 105
当你定义一个绑定而不给它一个值时,触手就没有东西可以抓住,所以它会以虚无的方式结束。如果你询问一个空绑定的值,你将得到值 undefined
。
let one = 1, two = 2; console.log(one + two); // → 3
词语 var
和 const
也可以用来创建绑定,类似于 let
。
var name = "Ayda"; const greeting = "Hello "; console.log(greeting + name); // → Hello Ayda
第一个,var
(“变量”的缩写),是 2015 年之前的 JavaScript 中声明绑定的方式,当时 let
还不存在。我将在 下一章 中回到它与 let
的精确区别。现在,请记住,它基本上做了同样的事情,但我们在这本书中很少使用它,因为它在某些情况下行为异常。
词语 const
代表常量。它定义了一个常量绑定,该绑定在它生存的整个时间段内指向同一个值。这对于仅为值提供名称的绑定很有用,以便你可以在以后轻松地引用它。
绑定名称
绑定名称可以是任何一个或多个字母的序列。数字可以是绑定名称的一部分——例如 catch22
是一个有效的名称——但名称不能以数字开头。绑定名称可以包含美元符号($
)或下划线(_
),但不能包含其他标点符号或特殊字符。
具有特殊含义的词,例如 let
,是关键字,不能用作绑定名称。还有一些词语“保留供将来版本的 JavaScript 使用”,这些词语也不能用作绑定名称。关键字和保留字的完整列表相当长
break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import interface in instanceof let new package private protected public return static super switch this throw true try typeof var void while with yield
不用担心记住这个列表。当创建绑定产生意外的语法错误时,请检查是否正在尝试定义一个保留字。
环境
在给定时间存在的绑定及其值的集合称为环境。当程序启动时,此环境并不为空。它总是包含语言标准的一部分的绑定,并且大多数情况下,它也包含提供与周围系统交互方式的绑定。例如,在浏览器中,存在与当前加载的网站交互以及读取鼠标和键盘输入的函数。
函数
默认环境中提供的许多值都是函数类型。函数是包装在值中的程序片段。这些值可以被应用,以便运行包装的程序。例如,在浏览器环境中,绑定 prompt
保存一个函数,该函数显示一个小型对话框,要求用户输入。它使用方法如下
prompt("Enter passcode");
执行函数称为调用、执行或应用它。你可以通过在产生函数值的表达式后面加上括号来调用函数。通常你会直接使用保存函数的绑定的名称。括号之间的值被传递给函数内部的程序。在这个例子中,prompt
函数使用我们给它的字符串作为在对话框中显示的文本。传递给函数的值称为参数。不同的函数可能需要不同数量或不同类型的参数。
prompt
函数在现代 Web 编程中使用不多,主要是因为你无法控制生成的对话框的外观,但它在玩具程序和实验中很有用。
console.log 函数
在示例中,我使用 console.log
来输出值。大多数 JavaScript 系统(包括所有现代 Web 浏览器和 Node.js)提供一个 console.log
函数,该函数将其参数写入某些文本输出设备。在浏览器中,输出将出现在 JavaScript 控制台中。浏览器界面的这一部分默认情况下是隐藏的,但大多数浏览器在按下 F12 或在 Mac 上按下 command-option-I 时会打开它。如果这不起作用,请在菜单中搜索名为开发者工具或类似工具的项目。
在本书页面上运行示例(或你自己的代码)时,console.log
输出将显示在示例之后,而不是在浏览器的 JavaScript 控制台中。
let x = 30; console.log("the value of x is", x); // → the value of x is 30
虽然绑定名称不能包含句点字符,但 console.log
确实包含一个。这是因为 console.log
不是一个简单的绑定,而是一个表达式,它从 console
绑定中保存的值中检索 log
属性。我们将在 第 4 章 中找出这到底是什么意思。
返回值
显示对话框或在屏幕上写入文本是副作用。许多函数之所以有用是因为它们产生的副作用。函数也可以生成值,在这种情况下,它们不需要产生副作用才能有用。例如,函数Math.max
接受任意数量的数字参数并返回最大的值。
console.log(Math.max(2, 4)); // → 4
当函数生成值时,据说它返回该值。在 JavaScript 中,任何产生值的都是表达式,这意味着函数调用可以在更大的表达式中使用。在下面的代码中,对Math.min
(与Math.max
相反)的调用用作加号表达式的部分
console.log(Math.min(2, 4) + 100); // → 102
第 3 章将解释如何编写自己的函数。
控制流
当您的程序包含多个语句时,这些语句的执行方式就像一个故事,从上到下。例如,以下程序包含两个语句。第一个要求用户输入一个数字,第二个在第一个执行之后执行,显示该数字的平方
let theNumber = Number(prompt("Pick a number")); console.log("Your number is the square root of " + theNumber * theNumber);
函数Number
将值转换为数字。我们需要这种转换,因为prompt
的结果是一个字符串值,而我们想要一个数字。还有类似的函数称为String
和Boolean
,它们将值转换为这些类型。
条件执行
并非所有程序都是直路。例如,我们可能希望创建一个分支道路,程序根据当前情况选择正确的分支。这称为条件执行。
条件执行是在 JavaScript 中使用if
关键字创建的。在简单的情况下,我们希望在且仅当某个条件成立时才执行一些代码。例如,我们可能希望仅当输入实际上是一个数字时才显示输入的平方
let theNumber = Number(prompt("Pick a number")); if (!Number.isNaN(theNumber)) { console.log("Your number is the square root of " + theNumber * theNumber); }
通过这种修改,如果您输入“parrot”,则不会显示任何输出。
if
关键字根据布尔表达式的值执行或跳过语句。决定表达式写在关键字之后,括号之间,后面是执行的语句。
Number.isNaN
函数是标准 JavaScript 函数,仅当提供的参数为NaN
时返回true
。Number
函数碰巧在您提供不代表有效数字的字符串时返回NaN
。因此,条件转换为“除非theNumber
不是数字,否则执行此操作”。
在这个示例中,if
之后的语句用大括号({
和 }
)包裹。大括号可以用来将任意数量的语句分组到单个语句中,称为块。您也可以在这种情况下省略它们,因为它们只包含单个语句,但为了避免思考是否需要它们,大多数 JavaScript 程序员在所有类似的包装语句中使用它们。我们在这本书中大部分都会遵循这个惯例,除了偶尔的单行语句。
if (1 + 1 == 2) console.log("It's true"); // → It's true
您通常不仅会有在条件为真时执行的代码,还会处理其他情况的代码。这个备用路径由图中的第二个箭头表示。您可以使用else
关键字与if
一起,创建两个独立的、备用的执行路径
let theNumber = Number(prompt("Pick a number")); if (!Number.isNaN(theNumber)) { console.log("Your number is the square root of " + theNumber * theNumber); } else { console.log("Hey. Why didn't you give me a number?"); }
如果您有多条路径可供选择,您可以将多个if
/else
对“链接”在一起。以下是一个示例
let num = Number(prompt("Pick a number")); if (num < 10) { console.log("Small"); } else if (num < 100) { console.log("Medium"); } else { console.log("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 的偶数,这种方法将不可行。我们需要的是一种方法来运行一段代码多次。这种形式的控制流称为循环。
循环控制流允许我们回到程序中之前的位置并重复执行它,并使用当前的程序状态。如果我们将其与计数绑定结合起来,我们可以执行以下操作
let number = 0; while (number <= 12) { console.log(number); number = number + 2; } // → 0 // → 2 // … etcetera
以关键字while
开头的语句创建一个循环。while
后面是括号中的表达式,然后是一个语句,很像if
。只要表达式产生的值在转换为布尔值时给出true
,循环就会一直进入该语句。
number
绑定演示了绑定如何跟踪程序的进度。每次循环重复时,number
都将获得一个比其先前值大 2 的值。在每次重复开始时,它将与数字 12 进行比较,以确定程序的工作是否已完成。
作为一个实际上可以做一些有用事情的例子,我们现在可以编写一个程序来计算并显示 210(2 的 10 次方)的值。我们使用两个绑定:一个跟踪我们的结果,另一个计算我们已经将该结果乘以 2 的次数。循环测试第二个绑定是否已达到 10,如果尚未达到,则更新两个绑定。
let result = 1; let counter = 0; while (counter < 10) { result = result * 2; counter = counter + 1; } console.log(result); // → 1024
计数器也可以从1
开始并检查<= 10
,但由于一些将在第 4 章中变得明显的原因,习惯从 0 开始计数是一个好主意。
请注意,JavaScript 还有一个用于求幂的运算符 (2 ** 10
),您将在实际代码中使用它来计算这个值,但这会破坏示例。
do
循环是一种与while
循环类似的控制结构。它仅在一点上有所不同:do
循环始终至少执行一次其主体,并且它只在第一次执行后开始测试是否应该停止。为了反映这一点,测试出现在循环主体的后面
let yourName; do { yourName = prompt("Who are you?"); } while (!yourName); console.log("Hello " + yourName);
此程序将强制您输入姓名。它将反复询问,直到获得不是空字符串的名称。应用!
运算符将在否定之前将值转换为布尔类型,除了""
之外的所有字符串都将转换为true
。这意味着循环将继续执行,直到您提供非空的姓名。
缩进代码
在示例中,我一直会在作为某个较大语句一部分的语句前面添加空格。这些空格不是必需的——计算机在没有它们的情况下也能正常接受程序。实际上,程序中的换行符也是可选的。如果您愿意,可以将程序写成一行。
此缩进在块中的作用是使代码的结构突出显示给人类读者。在代码中,新的块在其他块内打开,可能难以看到一个块的结束和另一个块的开始。通过适当的缩进,程序的视觉形状与内部块的形状相对应。我喜欢为每个打开的块使用两个空格,但口味不同——有些人使用四个空格,有些人使用制表符。重要的是,每个新块添加相同数量的空格。
if (false != true) { console.log("That makes sense."); if (1 < 2) { console.log("No surprise there."); } }
大多数代码编辑器程序(包括这本书中的代码编辑器)将通过自动将新行缩进适当数量来提供帮助。
for 循环
许多循环遵循while
示例中所示的模式。首先创建一个“计数器”绑定来跟踪循环的进度。然后是一个while
循环,通常带有一个测试表达式,用于检查计数器是否已达到其结束值。在循环主体结束时,计数器更新以跟踪进度。
由于此模式非常常见,JavaScript 和类似语言提供了稍微更短且更全面的形式,即for
循环
for (let number = 0; number <= 12; number = number + 2) { console.log(number); } // → 0 // → 2 // … etcetera
此程序与前面的偶数打印示例完全相同。唯一的变化是将所有与循环“状态”相关的语句分组到for
之后。
for
关键字后面的括号必须包含两个分号。第一个分号之前的部分初始化循环,通常是通过定义绑定。第二部分是表达式,用于检查循环是否必须继续。最后部分在每次迭代后更新循环的状态。在大多数情况下,这比while
结构更短更清晰。
let result = 1; for (let counter = 0; counter < 10; counter = counter + 1) { result = result * 2; } console.log(result); // → 1024
跳出循环
循环条件产生false
并非循环结束的唯一方式。break
语句具有立即跳出封闭循环的效果。以下程序演示了它的使用,该程序找到第一个大于或等于 20 且可被 7 整除的数字。
for (let current = 20; ; current = current + 1) { if (current % 7 == 0) { console.log(current); break; } } // → 21
使用余数 (%
) 运算符是测试一个数字是否可以被另一个数字整除的简单方法。如果是,则它们除法的余数为零。
示例中的for
结构没有检查循环结束的部分。这意味着循环永远不会停止,除非执行其中的break
语句。
如果你要删除该break
语句,或者你意外地写了一个总是产生true
的结束条件,你的程序就会陷入无限循环。陷入无限循环的程序永远不会结束运行,这通常是一件坏事。
如果你在这些页面上的示例之一中创建了一个无限循环,通常会问你是否要在几秒钟后停止脚本。如果失败,你将不得不关闭正在使用的选项卡才能恢复。
continue
关键字类似于break
,因为它影响循环的进度。当在循环体中遇到continue
时,控制权将跳出循环体并继续进行循环的下一个迭代。
简洁地更新绑定
尤其是在循环时,程序通常需要“更新”一个绑定,以根据该绑定的先前值来保存一个值。
counter = counter + 1;
counter += 1;
类似的快捷方式适用于许多其他运算符,例如result *= 2
将result
加倍,或者counter -= 1
向下计数。
for (let number = 0; number <= 12; number += 2) { console.log(number); }
对于counter += 1
和counter -= 1
,还有更短的等效项:counter++
和counter--
。
使用 switch 根据值进行调度
if (x == "value1") action1(); else if (x == "value2") action2(); else if (x == "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 有两种编写注释的方式。要编写单行注释,你可以使用两个斜杠字符 (//
),然后是它后面的注释文本
let accountBalance = calculateBalance(account); // It's a green hollow where a river sings accountBalance.adjust(); // Madly catching white tatters in the grass. let 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 an old notebook. 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. */ const myNumber = 11213;
总结
你现在知道程序是由语句构成的,语句本身有时包含更多语句。语句往往包含表达式,表达式本身可以由更小的表达式构成。
将语句一个接一个地放在一起,就会得到一个从上到下执行的程序。你可以使用条件 (if
、else
和switch
) 和循环 (while
、do
和for
) 语句来引入控制流中的干扰。
绑定可以用来将数据片段存储在一个名称下,它们对于跟踪程序中的状态很有用。环境是定义的一组绑定。JavaScript 系统总是将一些有用的标准绑定放到你的环境中。
函数是封装程序片段的特殊值。你可以通过编写functionName(argument1, argument2)
来调用它们。这样的函数调用是一个表达式,它可能会产生一个值。
练习
如果你不确定如何测试练习的解决方案,请参考介绍。
每个练习都从一个问题描述开始。阅读此描述并尝试解决练习。如果你遇到问题,请考虑在练习后阅读提示。你可以在网上找到练习的完整解决方案,网址为https://eloquent.javascript.ac.cn/code。如果你想从练习中学到东西,我建议你只有在解决练习后,或者至少在你长时间努力解决练习并感到头痛后,才查看解决方案。
循环三角形
编写一个循环,对console.log
进行七次调用,以输出以下三角形
# ## ### #### ##### ###### #######
了解你可以通过在字符串后面写.length
来查找字符串的长度可能很有用。
let 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"
而不是数字。
当你完成上述工作后,修改你的程序,对于可以被 3 和 5 整除的数字,打印"FizzBuzz"
(对于只可以被其中一个整除的数字,仍然打印"Fizz"
或"Buzz"
)。
(这实际上是一个面试问题,据称它剔除了很大一部分程序员候选人。所以如果你解决了它,你的劳动力市场价值就提高了。)
// Your code here.
显示提示...
棋盘
编写一个程序,使用换行符分隔行,创建一个表示 8×8 网格的字符串。网格的每个位置要么是空格,要么是“#”字符。这些字符应该形成一个棋盘。
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
当你有一个生成这种模式的程序时,定义一个绑定size = 8
,并更改程序使其适用于任何size
,输出给定宽度和高度的网格。
// Your code here.