學習目標
- 理解變量、循環、循環條件和循環體的概念
- 了解控制臺和日志的概念
- 畫出線和方塊
學習用時:60分鐘
通過上節課的學習,我們已經掌握了如何通過畫一個個的點來組成圖案。很多同學在作業中都畫出了漂亮的圖案,深刻地領悟到了像素的威力。
相信大家都發現了一個問題:有一片區域的點顏色是完全一樣的,但由于上節課我特別強調過,不讓大家用fillRect來畫線和長方形,于是只能一個點、一個點地畫出來,復制一大堆語句,然后改成一連串的坐標……
其實我們在屏幕上看到的所有東西,也都是電腦一個點接一個點畫出來的,只不過它畫得非常快,我們察覺不到它畫的過程而已。
由于我們使用的畫布尺寸很小(縮放比例為10時,尺寸為40x40),大多數同學的圖案也都在15x15的范圍以內,所以重復的代碼量還是可以接受的。同學們咬咬牙,也就做出來了。但是在今天動轍720p、1080p甚至4K的分辨率下,一張畫面中的像素數量幾乎是個天文數字,想要一個點一個點地畫,就幾乎是不可能完成的任務了。
這就像我們拿著一支筆在紙上畫畫,卻被要求只能一個點接一個點地把圖案“戳”出來一樣,很蛋疼是不是?有同學問了:為什么我們不使用現成的fillRect工具,把筆拿起來“畫”呢?
我們只有在失去一件東西時,才會真正領悟到它存在的意義。正如沒有自己親手洗過衣服的人,無法認識到洗衣機的重要性一樣。要是你沒有親身體驗過這種機械重復的“痛苦”,你就無法真正認識到我們今天所學內容的價值。
重復重復再重復
編程的主要意義之一,就是消除重復。對此我們有一條非常著名的編程原則:
Don’t Repeat Yourself:不要重復你自己
我們可以運用編程思維,對一件需要重復進行的工作進行分析和提煉,然后精簡成用有限語句構成的表達,來讓機器執行。接下來,我就來教大家怎樣畫出一條線,進而畫出一個方塊。
我們先來看看,在日常生活中有哪些不斷重復的事情?
- 吃飯:一口接一口,吃飽為止……(明確的目標)
- 做俯臥撐:20次一組,1、2、3……(明確的數量目標)
- 洗草莓:一顆一顆地洗,洗完為止……(遍歷一個集合)
- ……
以上這些事情,雖然表面上的形式完全不同,但其中的模式都是相同的:
- 通過無數次重復的動作完成
- 每次執行的動作大同小異
- 有明確的目標:吃飽 / 做夠數量的俯臥撐 / 洗完所有的草莓
- ……
當我們做這些事情時,其實是在進行這樣的流程:
- 判斷目標是否已經達成:吃飽了沒?/ 做夠了沒?/ 都洗完了嗎?
- 已達成——結束流程:不吃了 / 不做了 / 不洗了
- 未達成——執行動作:吃一口飯 / 做一個俯臥撐 / 洗一顆草莓
這樣的重復流程,在編程中叫做循環(Loop)。
循環(Loop):一段在程序中只出現一次,但可能會連續運行多次的代碼。
事實上,任何一個需要持續進行的流程都是循環:
- 走路:走到目的地了嗎?沒走到,那繼續走,走到為止……
- 看書:看完了沒有?沒看完,那繼續看,看完為止……
- 睡覺:睡夠了沒有?沒睡夠,那繼續睡,睡夠為止……
- ……
循環什么時候停止?
循環的本質是滿足條件則重復。比如就吃飯來說,當我們已經吃飽的時候,就沒有什么動力再繼續吃了。也就是說,我們繼續吃下一口飯的前提是還沒吃飽,我們把這種前提稱為循環條件(Condition)。
循環條件(Condition):讓循環繼續重復執行的條件
一般來說,循環條件也可以表現為一個只能用“是”或“否”回答的問題。比如在吃飯的例子中,循環條件也可以表現為:“是不是沒吃飽?”
在生活中,如果我們在執行重復流程前發現目標設立的不合理(比如洗一整卡車的草莓),那我們可能一開始就會拒絕執行。如果目標一開始貌似可行,但在執行的過程中發現是不可能的(比如吃完一只烤羊腿),或者遇到了意外情況(吃到蒼蠅 / 發生車禍 / 咖啡倒書上了……),我們也可能會讓循環提前中止。
即便是頭驢子,在發現無論如何都吃不到吊在面前的胡蘿卜時,它也會放棄。但是電腦就沒有這么聰明了,程序中的循環條件再怎么不合理它也會乖乖地執行,并會不知疲倦地一直運行下去。如果循環條件永遠成立,循環就不會終止,就會形成死循環(Infinite Loop)。
死循環(Infinite Loop):因循環條件永遠成立而不會停止運行的循環
“死循環”聽上去是個可怕的概念。事實上,由于程序設計漏洞意外造成的死循環,可能會讓電腦卡頓甚至死機。好在現在大多數操作系統都能處理這種情況,在這種情況下會友好地提示我們終止程序:
循環執行時都做些什么?
吃飯時我們重復的動作是什么呢?吃一口菜,吃一口飯,嚼一嚼,咽下去……這是在吃飯過程中要重復進行的流程,我們稱之為循環體(Statement)。
循環體(Statement):每次循環時所執行的流程
循環體可以是空的。比如在睡覺的時候,我們什么也不做。又比如在燒開水的過程中,我們僅僅是在等待而已。
對一開始就給定次數的循環(比如做俯臥撐),我們需要在每次循環時更新計數,這樣才能知道什么時候結束循環。
對遍歷一個集合的循環(比如洗一盤子草莓),我們可能需要不斷減少集合里的成員(比如把洗好的草莓放到另一個盤子里),又或者給處理過的成員做標記以區分(比如把洗好的草莓葉子都摘掉)。
在一次循環中,循環體到底重復執行多少次,我們可能知道(比如做俯臥撐),也可能不知道(比如吃飯),也可能要做完了才知道(比如洗草莓)。
怎么畫一個方塊?
現在回到我們的主題:我們要畫一個方塊。但由于不能使用fillRect的后兩個參數,所以我們現在只能畫出一個點,除了寫一大堆畫點語句之外,目前我們對用其他方法來完成這個任務還沒有任何頭緒。
是時候祭出殺手锏了,讓我們來對這個任務進行拆分。
我們知道,在屏幕上的一個方塊是由一大堆點構成的。這些點可以視為緊緊排在一起的一堆線,可以是一堆垂直方向的線,也可以是一堆水平方向的線。所以我們可以把任務拆分為畫出一堆線。具體的流程是:
- 畫出一條線
- 知道下一條線畫在哪里
- 重復以上兩步,在畫夠數量之后停下來
線則是由同一方向的點構成的,所以我們可以繼續拆分為畫出一堆點。具體的流程是:
- 畫一個點
- 知道下一個點畫在哪里
- 重復以上兩步,在畫夠數量之后停下來
畫一個點我們已經會了;下一個點的坐標位置我們也可以通過簡單的加減計算得出;需要畫的數量就是方塊的尺寸。于是我們只要完成“重復以上兩步”的功能,就可以畫出一條線,進而畫出方塊了。
那這個“重復以上兩步”怎么完成?這就需要用到我們剛學過的循環了。
初見循環
首先請在電腦上的Chrome瀏覽器中打開 http://codepen.io/zhangshenjia/pen/JWeWON,你會看到這樣的界面:
我們可以看到,畫布上有兩條水平線。再看看代碼:調縮放比例、調顏色、畫點……這些都是我們上節課玩過的東西。在最后面有三行代碼是我們沒見過的:
這是JS里的一個for循環。
除了for循環外,在JS語言中還有while、do/while、for in循環,感興趣的朋友可以查看相關的文檔:http://www.w3school.com.cn/js/js_loop_for.asp
在 { 和 } 之前的代碼,就是這個循環的循環體。細心的同學可能發現了,這行代碼前面多加了兩個空格,這是為了表現代碼層次關系而添加的縮進。行首的空格對程序的運行來說沒有任何實際影響,但能讓人更容易理解代碼的結構。
有的程序員喜歡在行首增加兩個空格,有的程序員喜歡增加四個空格,還有的程序員喜歡在行首添加TAB制表符……孰優孰劣,難有定論。但可以確定的是,在一份代碼里應該始終使用相同的縮進風格。
等下,這行語句好像很眼熟……這不就是我們用過的畫點語句嗎?沒錯,就是它。但是我們仔細看看,就會發現有點區別:第一個數字變成字母 i 了。
可能有的同學在上節課改代碼時曾經試過(什么,你沒試過?現在試試看!),這里必須寫數字,如果改成字母,整個程序就會出問題的。比如我們把第一行畫點語句里第一個數字改成字母 a 看看:
我的乖乖,整個畫布都空了,這說明程序出問題了,趕緊改回來吧。那循環里的畫點語句為什么就能這么寫呢?我們可以把它改成數字 0 看看:
這下可好,第二條線變成一個點了,為啥呢?想想就會知道,每次執行循環的時候,都在0, 10這個固定的坐標位置上畫點,結果可不就重到一起了嘛!我們可以看看前面的那堆畫點語句(它們畫出了畫布最上方的那條線),可以看到第一個數字一直在發生變化:
把我們剛才的修改還原一下,第二條線就又出現了。事實上,每次循環時,這個 i 都在發生變化,它是一個變量(Variable)。
變量(Variable):可以用來保存和訪問數據的具名地址
變量可以理解為我們做菜時,用來盛裝食材的盤子。一開始所有的盤子都是空的,在準備炒菜的過程中,我們會把食材、調料等盛到不同的盤子里,然后按需取用。同一個盤子可能一會用來存放切好的蒜末,一會又用來盛拌好的涼菜……
做一頓飯可能會用到很多盤子,我們需要記住其中某些盤子的作用,比如這個盤子曾經裝過生肉,那就不能用來裝熟食。在多人協作的廚房里,盤子可能超級多,單純靠個人的記憶就不太好使了,這時候就需要給盤子夾上標簽來做記號(吃過麻辣香鍋或羊肉泡饃的同學應該都見過)。
同樣,程序里也可以有很多變量,所以我們也必須給變量進行命名,這樣才能通過名字來使用它,也能通過名字得知變量的用途。這個循環變量的變量名就是 i。
為什么循環里的變量的名字要叫作 i 呢?這是個有趣的問題,你可以在課后搜索研究一下。
我們把每次需要畫點的水平坐標存放在變量 i 里,需要畫點的時候再把水平坐標從變量 i 里讀取出來使用。在每次循環時,我們都改變它的值,這樣就可以畫出一條線來了。
改改改!
接下來,讓我們通過試探性地修改,來了解for循環中各部分的作用。首先,把for循環中出現的第二個數字 5 改成 10,看看會發生什么:
可以看到第二條線延長了一倍。本來想要實現這樣的效果,我們是需要多寫5條畫點語句的。而現在只需要改一個數字就行了,有沒有很爽?看來這個數字決定了線條的長度,也就是循環執行多少次。
事實上,i < 10 就是循環的循環條件。在每次執行循環體前,都會對它進行判斷,只有在循環條件成立的情況下,循環體才會被執行;如果不成立,循環就會結束。
要是把循環條件刪掉,或者改成 i > 0 會怎么樣?如果沒有循環條件,或者循環條件一直都滿足,就會死循環噢!你可以試試看(試完了記得把代碼改回來)……放心,Chrome瀏覽器會檢查出死循環并終止它,不會死機的。
我們再把第一個數字 0 改成 5 試試看:
可以看到第二條線變短且向右移動了。這個數字決定了我們第一個點從哪里開始畫。如果你把這個數字改成一個很大的數,比如說 100,你會發現一個點都沒有畫出來,因為循環條件 i < 10 得不到滿足,循環一次都不會被執行。
事實上,var i = 5是循環的初始化語句,它只在循環一開始執行一次,聲明了一個變量 i 并給它賦一個初始值。這就相當于我們從櫥柜拿出來一個盤子洗干凈,裝了點東西。
要是把初始化語句刪掉,會怎么樣呢?由于變量 i 沒有聲明,在判斷循環條件時程序就會報錯,循環根本就不會被執行。
讓我們來研究下括號里的最后一部分,先把最后的數字 1 改成 2 看看:
恩,怎么變成虛線了?這個數字決定了下一個點畫在哪里。你可以通過修改這個數字來調整兩個點之間的距離(還可以是小數噢)。
事實上,* i = i + 1* 是循環的遞增語句,它在每次循環體執行結束后都會被執行。一般情況下,遞增語句主要的任務就是對循環變量進行更新,確保循環不會變成死循環。
i = i + 1 還可以寫成 i += 1 和 **i++ **,感興趣的朋友可以看看JS運算符的文檔:http://www.w3school.com.cn/js/js_operators.asp
for循環是按照什么流程執行的?
- 先執行初始化語句;
- 判斷循環條件,如果條件不滿足,則退出循環;
- 如果條件滿足,則執行循環體;
- 執行遞增語句,然后跳到第二步。
我們可以在腦中模擬執行一下我們的代碼,看看循環是怎么工作的:
循環剛開始時 i = 5,這當然符合 i < 10 的循環條件,于是畫點語句被執行,在坐標 5, 10 的位置畫了一個點。隨后遞增語句 i = i + 1 執行,i 的值變為6,繼續循環。
第二次循環時 i = 6,循環條件 i < 10 依然成立,于是又在坐標 6,10 的位置畫了一個點,隨后 i 的值變為7。
……
第六次循環時 i = 10,此時循環條件 i < 10 已經不成立了,于是循環結束。
用嵌套循環來畫方塊
好了,現在我們已經能用for循環來畫出N個點來組成一條水平線了,那怎么更進一步,畫出N條水平線來組成一個方塊呢?
想要把畫線的代碼重復執行N次,還是需要使用循環。不過這次我們的循環體不再是畫點的代碼了,而是畫線代碼——即我們剛才研究過的循環。
啥,循環體還可以是個循環?有點暈了,讓我喘口氣先……沒事,使勁喘,喘完了我們再繼續。
一個盒子里的空間可以用來裝尺寸合適的任何東西,當然也可以用來裝另一個盒子。同樣,循環體是由代碼組成的,而整個循環本來就是一段代碼,所以我們當然也可以在循環體里使用循環,這叫做嵌套(Nesting)。
嵌套(Nesting):在一個結構體內部包含另一個結構體
我們可以通過嵌套循環來解決多維度上的問題,每層循環處理一個維度上的變化,將問題“降維”之后在循環體內進一步解決。在我們今天的例子里,我們要在二維平面上畫一個方塊,就可以通過兩層嵌套循環來實現,內層在水平方向循環(畫點成線),外層在垂直方向循環(畫線成面)。現在實現畫點成線功能的內層循環我們已經有了,我們需要在它外面再寫一個外層循環。
首先,請刷新一下頁面,把代碼恢復成原樣,然后把第8-12行的畫點語句刪掉。這樣屏幕上就只剩下用for循環畫的那條線了。在for語句前面插入一個空行,然后照貓畫虎寫一條for語句,然后在循環后面插入一個空行,輸入 } :
注意:在程序中出現的所有符號都是半角符號(如(;){),推薦在編程時關閉中文輸入法,避免不小心輸入全角符號(如(;){),導致語法錯誤。
現在代碼的層次結構并不是很清楚,讓我們選中內層循環的那三行,按一下 TAB 鍵,給它們增加縮進,這樣看上去就容易理解多了:
等等,怎么畫出來的還是條直線呢?因為我們循環里的畫點語句只更改了x坐標,y坐標一直都是10。現在我們把畫點語句里的第二個數字 10 改成 i 看看:
這下我們畫出了一條斜線,還不是方塊,這是為什么呢?想想就能明白,水平和垂直方向坐標相同,畫出來的點就是(1, 1)、(2、2)、……連起來可不就是一條斜線嘛!我們想讓水平和垂直坐標分別進行變化,只用一個變量 i 肯定是不行的,需要再增加一個變量。讓我們把內層循環里的 i 改成 j,然后再把畫點語句里的第二個 i 也改成 j:
耶,我們的方塊終于畫出來了!
為什么第二層循環的變量要取名為 j 呢?這也是一個慣例,如果有多層循環,會從 i 開始按順序使用字母作為循環變量。
通過輸出日志跟蹤程序的運行情況
代碼的結構層級越多,就越難搞明白它是怎么運作的。如果只有一層循環,還可以通過純粹的思考來模擬,畢竟只有一個循環變量在發生變化。但在多層嵌套循環里這樣做就難多了,因為你需要記住每一層循環的當前循環次數以及對應循環變量的當前值。
另外,隨著嵌套循環層級的增多,最內部循環體的運行次數是呈指數增長的。像我們剛才僅僅畫一個5x5的方塊,內部的畫點語句就執行了25次。想要跟蹤每一步運行的情況不是不可以,而是太繁瑣了。
幸好,我們有其他方法可以用來跟蹤復雜程序的運行過程,那就是在控制臺輸出日志。
控制臺(Console):一個交互界面,可以用來顯示程序運行中的錯誤信息和調試打印日志
日志(Log):在程序運行過程中,按照指定的需求和格式留下的數據記錄
讓我們先在循環里的畫點語句后面新增一行,把下面的代碼復制過去:
console.log('i=' + i + ', j=' + j);
這行代碼的作用是,在每次執行循環體時,在控制臺把循環變量 i 和 j 的值打印出來。接下來我們就要打開控制臺,看看輸出的日志:
首先按下 F12(Mac電腦按下Command + Option + I),或者在網頁的任何位置點擊鼠標右鍵,然后選擇最下面的“檢查”項,打開開發人員工具:
有可能你的開發人員工具出現在瀏覽器下面,或者是一個獨立的窗口。你可以在界面的右上角的關閉按鈕旁邊找到設置功能,選擇第三個布局,這樣它就會固定在瀏覽器的右邊了。
開發人員工具默認顯示的是 Elements 選項卡,這是用來顯示網頁元素結構的,我們現在用不著。請切換到如圖所示的 Console 選項卡,就能看到一堆我們輸出的日志了:
通過跟蹤日志輸出,我們就可以知道每一次循環時,變量 i 和 j 分別的變化情況。有同學問:這里面的日志太多了,分不清哪些是之前輸出的,哪些是這次輸出的,該怎么辦呢?
好辦,我們可以點擊左上角的清除按鈕把所有的日志都干掉,再對程序進行一次無所謂的改動(比如在一個空行里加個空格)讓它重新運行一次,這樣新出現的日志就都是本次運行輸出的了。
除了顯示錯誤信息和記錄調試日志外,控制臺還可以直接輸入JS代碼進行執行,有興趣的朋友可以自己研究體驗一下。
內容回顧
本節課所學的概念:
循環(Loop):一段在程序中只出現一次,但可能會連續運行多次的代碼。
循環條件(Condition):讓循環繼續重復執行的條件
循環體(Statement):每次循環時所執行的流程
死循環(Infinite Loop):因循環條件永遠成立而不會停止運行的循環
變量(Variable):可以用來保存和訪問數據的具名地址
嵌套(Nesting):在一個結構體內部包含另一個結構體
控制臺(Console):一個交互界面,可以用來顯示程序運行中的錯誤信息和調試打印日志
日志(Log):在程序運行過程中,按照指定的需求和格式留下的數據記錄
本節課所學的原則:
Don’t Repeat Yourself:不要重復你自己
課后作業
1、找出至少一個在生活中循環的案例,并明確循環條件和循環體,比如:
循環:吃飯
循環條件:沒吃飽
循環體:吃一口
2、配合使用循環和畫點語句,畫出更復雜的圖案。