二、程式結構
原文: Program Structure 譯者: 飛龍 協定: CC BY-NC-SA 4.0 自豪地采用 谷歌翻譯 部分參考了 《JavaScript 程式設計精解(第 2 版)》And my heart glows bright red under my filmy, translucent skin and they have to administer 10cc of JavaScript to get me to come back. (I respond well to toxins in the blood.) Man, that stuff will kick the peaches right out your gills!
why,《Why’s (Poignant) Guide to Ruby》
在本章中,我們開始做一些實際上稱為程式設計的事情。 我們将擴充我們對 JavaScript 語言的掌控,超出我們目前所看到的名詞和句子片斷,直到我們可以表達有意義的散文。
表達式和語句
在第 1 章中,我們為它們建立了值,并應用了運算符來獲得新的值。 像這樣建立值是任何 JavaScript 程式的主要内容。 但是,這種東西必須在更大的結構中建構,才能發揮作用。 這就是我們接下來要做的。
我們把産生值的操作的代碼片段稱為表達式。按照字面含義編寫的值(比如
22
或
"psychoanalysis"
)都是一個表達式。而括号當中的表達式、使用二進制運算符連接配接的表達式或使用一進制運算符的表達式,仍然都是表達式。
這展示了一部分基于語言的接口之美。 表達式可以包含其他表達式,其方式非常類似于人類語言的從句嵌套 - 從句可以包含它自己的從句,依此類推。 這允許我們建構描述任意複雜計算的表達式。
如果一個表達式對應一個句子片段,則 JavaScript 語句對應于一個完整的句子。 一個程式是一列語句。
最簡單的一條語句由一個表達式和其後的分号組成。比如這就是一個程式:
1;
!false;
不過,這是一個無用的程式。 表達式可以僅僅滿足于産生一個值,然後可以由閉合的代碼使用。 一個聲明是獨立存在的,是以它隻有在影響到世界的時候才會成立。 它可以在螢幕上顯示某些東西 - 這可以改變世界 - 或者它可以改變機器的内部狀态,進而影響後面的語句。 這些變化被稱為副作用。 前面例子中的語句僅僅産生值
1
和
true
,然後立即将它們扔掉。 這給世界沒有留下什麼印象。 當你運作這個程式時,什麼都不會發生。
在某些情況下,JavaScript 允許您在語句結尾處省略分号。 在其他情況下,它必須在那裡,否則下一行将被視為同一語句的一部分。 何時可以安全省略它的規則有點複雜且容易出錯。 是以在本書中,每一個需要分号的語句都會有分号。 至少在你更了解省略分号的細節之前,我建議你也這樣做。
綁定
程式如何保持内部狀态? 它如何記住東西? 我們已經看到如何從舊值中産生新值,但這并沒有改變舊值,新值必須立即使用,否則将會再度消失。 為了捕獲和儲存值,JavaScript 提供了一種稱為綁定或變量的東西:
let caught = 5 * 5;
這是第二種語句。 關鍵字(keyword)
let
表示這個句子打算定義一個綁定。 它後面跟着綁定的名稱,如果我們想立即給它一個值,使用
=
運算符和一個表達式。
前面的語句建立一個名為
caught
的綁定,并用它來捕獲乘以
5 * 5
所産生的數字。
在定義綁定之後,它的名稱可以用作表達式。 這種表達式的值是綁定目前所持有的值。 這是一個例子:
let ten = 10;
console.log(ten * ten);
// → 100
當綁定指向某個值時,并不意味着它永遠與該值綁定。 可以在現有的綁定上随時使用
=
運算符,将它們與目前值斷開連接配接,并讓它們指向一個新值:
var mood = "light";
console.log(mood);
// → light
mood = "dark";
console.log(mood);
// → dark
你應該将綁定想象為觸手,而不是盒子。 他們不包含值; 他們捕獲值 - 兩個綁定可以引用相同的值。 程式隻能通路它還在引用的值。 當你需要記住某些東西時,你需要長出一個觸手來捕獲它,或者你重新貼上你現有的觸手之一。
我們來看另一個例子。 為了記住 Luigi 欠你的美元數量,你需要建立一個綁定。 然後當他還你 35 美元時,你賦予這個綁定一個新值:
let luigisDebt = 140;
luigisDebt = luigisDebt - 35;
console.log(luigisDebt);
// → 105
當你定義一個綁定而沒有給它一個值時,觸手沒有任何東西可以捕獲,是以它隻能捕獲空氣。 如果你請求一個空綁定的值,你會得到
undefined
值。
一個
let
語句可以同時定義多個綁定,定義必需用逗号分隔。
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
(“variable”的簡寫)是 JavaScript 2015 之前聲明綁定的方式。 我們在下一章中,會講到它與
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");
執行一個函數被稱為調用,或應用它(invoke,call,apply)。您可以通過在生成函數值的表達式之後放置括号來調用函數。 通常你會直接使用持有該函數的綁定名稱。 括号之間的值被賦予函數内部的程式。 在這個例子中,
prompt
函數使用我們提供的字元串作為文本來顯示在對話框中。 賦予函數的值稱為參數。 不同的函數可能需要不同的數量或不同類型的參數。
prompt
函數在現代 Web 程式設計中用處不大,主要是因為你無法控制所得對話框的外觀,但可以在玩具程式和實驗中有所幫助。
console.log
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
我們會在下一章當中講解如何編寫自定義函數。
控制流
當你的程式包含多個語句時,這些語句就像是一個故事一樣從上到下執行。 這個示例程式有兩個語句。 第一個要求使用者輸入一個數字,第二個在第一個之後執行,顯示該數字的平方。
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 (!isNaN(theNumber))
alert("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
您通常不會隻執行條件成立時代碼,還會處理其他情況的代碼。 該替代路徑由圖中的第二個箭頭表示。 可以一起使用
if
else
關鍵字,建立兩個單獨的替代執行路徑。
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", "0"));
if (num < 10) {
console.log("Small");
} else if (num < 100) {
console.log("Medium");
} else {
console.log("Large");
}
該程式首先會檢查
num
是否小于 10。如果條件成立,則執行顯示
"Small"
的這條路徑;如果不成立,則選擇
else
分支,
else
分支自身包含了第二個
if
。如果第二個條件即
num
小于 100 成立,且數字的範圍在 10 到 100 之間,則執行顯示
"Medium"
的這條路徑。如果上述條件均不滿足,則執行最後一條
else
分支路徑。
這個程式的模式看起來像這樣:
while
do
循環
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);
該程式确實可以工作,但程式設計的目的在于減少工作量,而非增加。如果我們需要小于 1000 的偶數,上面的方式是不可行的。我們現在所需的是重複執行某些代碼的方法,我們将這種控制流程稱為循環。
我們可以使用循環控制流來讓程式執行回到之前的某個位置,并根據程式狀态循環執行代碼。如果我們在循環中使用一個綁定計數,那麼就可以按照如下方式編寫代碼:
let number = 0;
while (number <= 12) {
console.log(number);
number = number + 2;
}
// → 0
// → 2
// … etcetera
循環語句以關鍵字
while
開頭。在關鍵字
while
後緊跟一個用括号括起來的表達式,括号後緊跟一條語句,這種形式與
if
語句類似。隻要表達式産生的值轉換為布爾值後為
true
,該循環會持續進入括号後面的語句。
number
綁定示範了綁定可以跟蹤程式進度的方式。 每次循環重複時,
number
的值都比以前的值多 2。 在每次重複開始時,将其與數字 12 進行比較來決定程式的工作是否完成。
作為一個實際上有用的例子,現在我們可以編寫一個程式來計算并顯示
2**10
(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 開始計數是個好主意。
do
循環控制結構類似于
while
循環。兩者之間隻有一個差別:
do
循環至少執行一遍循環體,隻有第一次執行完循環體之後才會開始檢測循環條件。
do
循環中将條件檢測放在循環體後面,正反映了這一點:
let yourName;
do {
yourName = prompt("Who are you?");
} while (!yourName);
console.log(yourName);
這個程式會強制你輸入一個名字。 它會一再詢問,直到它得到的東西不是空字元串。
!
運算符會将值轉換為布爾類型再取反,除了
""
之外的所有字元串都轉換為
true
。 這意味着循環持續進行,直到您提供了非空名稱。
代碼縮進
在這些例子中,我一直在語句前添加空格,它們是一些大型語句的一部分。 這些都不是必需的 - 沒有它們,計算機也會接受該程式。 實際上,即使是程式中的換行符也是可選的。 如果你喜歡,你可以将程式編寫為很長的一行。
塊内縮進的作用是使代碼結構顯而易見。 在其他塊内開啟新的代碼塊中,可能很難看到塊的結束位置,和另一個塊開始位置。 通過适當的縮進,程式的視覺形狀對應其内部塊的形狀。 我喜歡為每個開啟的塊使用兩個空格,但風格不同 - 有些人使用四個空格,而有些人使用制表符。 重要的是,每個新塊添加相同的空格量。
if (false != true) {
console.log("That makes sense.");
if (1 < 2) {
console.log("No surprise there.");
}
}
大多數代碼編輯器程式(包括本書中的那個)将通過自動縮進新行來提供幫助。
for
for
許多循環遵循
while
示例中看到的規律。 首先,建立一個計數器綁定來跟蹤循環的進度。 然後出現一個
while
循環,通常用一個測試表達式來檢查計數器是否已達到其最終值。 在循環體的末尾,更新計數器來跟蹤進度。
由于這種規律非常常見,JavaScript 和類似的語言提供了一個稍短而且更全面的形式,
for
循環:
for (let number = 0; number <= 12; number = number + 2)
console.log(number);
// → 0
// → 2
// … etcetera
該程式與之前的偶數列印示例完全等價。 唯一的變化是,所有與循環“狀态”相關的語句,在
for
之後被組合在一起。
關鍵字
for
後面的括号中必須包含兩個分号。第一個分号前面的是循環的初始化部分,通常是定義一個綁定。第二部分則是判斷循環是否繼續進行的檢查表達式。最後一部分則是用于每個循環疊代後更新狀态的語句。絕大多數情況下,
for
循環比
while
語句更簡短清晰。
下面的代碼中使用了
for
循環代替
while
循環,來計算
2**10
:
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 (let current = 20; ; current++) {
if (current % 7 == 0)
break;
}
}
// → 21
我們可以使用餘數運算符(
%
)來判斷一個數是否能被另一個數整除。如果可以整除,則餘數為 0。
本例中的
for
語句省略了檢查循環終止條件的表達式。這意味着除非執行了内部的
break
語句,否則循環永遠不會結束。
如果你要删除這個
break
語句,或者你不小心寫了一個總是産生
true
的結束條件,你的程式就會陷入死循環中。 死循環中的程式永遠不會完成運作,這通常是一件壞事。
如果您在(英文版)這些頁面的其中一個示例中建立了死限循環,則通常會詢問您是否要在幾秒鐘後停止該腳本。 如果失敗了,您将不得不關閉您正在處理的頁籤,或者在某些浏覽器中關閉整個浏覽器,以便恢複。
continue
關鍵字與
break
類似,也會對循環執行過程産生影響。循環體中的
continue
語句可以跳出循環體,并進入下一輪循環疊代。
更新綁定的簡便方法
程式經常需要根據綁定的原值進行計算并更新值,特别是在循環過程中,這種情況更加常見。
counter = counter + 1;
JavaScript 提供了一種簡便寫法:
counter += 1;
JavaScript 還為其他運算符提供了類似的簡便方法,比如
result*=2
可以将
result
變為原來的兩倍,而
counter-=1
counter
減 1。
這樣可以稍微簡化我們的計數示例代碼。
for (let number = 0; number <= 12; number += 2)
console.log(number);
對于
counter+=1
counter-=1
,還可以進一步簡化代碼,
counter+=1
可以修改為
counter++
,
counter-=1
counter--
switch
條件分支
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 one 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(arg1, arg2)
這種寫法來調用函數。函數調用可以是一個表達式,也可以用于生成一個值。
習題
如果你不清楚在哪裡可以找到習題的提示,請參考本書的簡介部分。
每個練習都以問題描述開始。 閱讀并嘗試解決這個練習。 如果遇到問題,請考慮閱讀練習後的提示。 本書不包含練習的完整解決方案,但您可以在
eloquentjavascript.net/code上線上查找它們。 如果你想從練習中學到一些東西,我建議僅在你解決了這個練習之後,或者至少在你努力了很長時間而感到頭疼之後,再看看這些解決方案。
LoopingaTriangle
編寫一個循環,調用 7 次
console.log
函數,列印出如下的三角形:
#
##
##
###
###
####
#####
這裡給出一個小技巧,在字元串後加上
.length
可以擷取字元串的長度。
let abc = "abc";
console.log(abc.length);
// → 3
FizzBuzz
編寫一個程式,使用
console.log
列印出從 1 到 100 的所有數字。不過有兩種例外情況:當數字能被 3 整除時,不列印數字,而列印
"Fizz"
。當數字能被 5 整除時(但不能被 3 整除),不列印數字,而列印
"Buzz"
當以上程式可以正确運作後,請修改你的程式,讓程式在遇到能同時被 3 與 5 整除的數字時,列印出
"FizzBuzz"
(這實際上是一個面試問題,據說剔除了很大一部分程式員候選人,是以如果你解決了這個問題,你的勞動力市場價值就會上升。)
棋盤
編寫一個程式,建立一個字元串,用于表示
8×8
的網格,并使用換行符分隔行。網格中的每個位置可以是空格或字元
"#"
。這些字元組成了一張棋盤。
将字元串傳遞給
console.log
将會輸出以下結果:
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
當程式可以産生這樣的輸出後,請定義綁定
size=8
,并修改程式,使程式可以處理任意尺寸(長寬由
size
确定)的棋盤,并輸出給定寬度和高度的網格。