学习目标
- 理解变量、循环、循环条件和循环体的概念
- 了解控制台和日志的概念
- 画出线和方块
学习用时:60分钟
通过上节课的学习,我们已经掌握了如何通过画一个个的点来组成图案。很多同学在作业中都画出了漂亮的图案,深刻地领悟到了像素的威力。
相信大家都发现了一个问题:有一片区域的点颜色是完全一样的,但由于上节课我特别强调过,不让大家用fillRect来画线和长方形,于是只能一个点、一个点地画出来,复制一大堆语句,然后改成一连串的坐标……
其实我们在屏幕上看到的所有东西,也都是电脑一个点接一个点画出来的,只不过它画得非常快,我们察觉不到它画的过程而已。
由于我们使用的画布尺寸很小(缩放比例为10时,尺寸为40x40),大多数同学的图案也都在15x15的范围以内,所以重复的代码量还是可以接受的。同学们咬咬牙,也就做出来了。但是在今天动辙720p、1080p甚至4K的分辨率下,一张画面中的像素数量几乎是个天文数字,想要一个点一个点地画,就几乎是不可能完成的任务了。
这就像我们拿着一支笔在纸上画画,却被要求只能一个点接一个点地把图案“戳”出来一样,很蛋疼是不是?有同学问了:为什么我们不使用现成的fillRect工具,把笔拿起来“画”呢?
我们只有在失去一件东西时,才会真正领悟到它存在的意义。正如没有自己亲手洗过衣服的人,无法认识到洗衣机的重要性一样。要是你没有亲身体验过这种机械重复的“痛苦”,你就无法真正认识到我们今天所学内容的价值。
重复重复再重复
Paste_Image.png编程的主要意义之一,就是消除重复。对此我们有一条非常著名的编程原则:
Don’t Repeat Yourself:不要重复你自己
我们可以运用编程思维,对一件需要重复进行的工作进行分析和提炼,然后精简成用有限语句构成的表达,来让机器执行。接下来,我就来教大家怎样画出一条线,进而画出一个方块。
我们先来看看,在日常生活中有哪些不断重复的事情?
- 吃饭:一口接一口,吃饱为止……(明确的目标)
- 做俯卧撑:20次一组,1、2、3……(明确的数量目标)
- 洗草莓:一颗一颗地洗,洗完为止……(遍历一个集合)
- ……
以上这些事情,虽然表面上的形式完全不同,但其中的模式都是相同的:
- 通过无数次重复的动作完成
- 每次执行的动作大同小异
- 有明确的目标:吃饱 / 做够数量的俯卧撑 / 洗完所有的草莓
- ……
当我们做这些事情时,其实是在进行这样的流程:
Paste_Image.png- 判断目标是否已经达成:吃饱了没?/ 做够了没?/ 都洗完了吗?
- 已达成——结束流程:不吃了 / 不做了 / 不洗了
- 未达成——执行动作:吃一口饭 / 做一个俯卧撑 / 洗一颗草莓
这样的重复流程,在编程中叫做循环(Loop)。
循环(Loop):一段在程序中只出现一次,但可能会连续运行多次的代码。
事实上,任何一个需要持续进行的流程都是循环:
- 走路:走到目的地了吗?没走到,那继续走,走到为止……
- 看书:看完了没有?没看完,那继续看,看完为止……
- 睡觉:睡够了没有?没睡够,那继续睡,睡够为止……
- ……
循环什么时候停止?
Paste_Image.png循环的本质是满足条件则重复。比如就吃饭来说,当我们已经吃饱的时候,就没有什么动力再继续吃了。也就是说,我们继续吃下一口饭的前提是还没吃饱,我们把这种前提称为循环条件(Condition)。
循环条件(Condition):让循环继续重复执行的条件
一般来说,循环条件也可以表现为一个只能用“是”或“否”回答的问题。比如在吃饭的例子中,循环条件也可以表现为:“是不是没吃饱?”
在生活中,如果我们在执行重复流程前发现目标设立的不合理(比如洗一整卡车的草莓),那我们可能一开始就会拒绝执行。如果目标一开始貌似可行,但在执行的过程中发现是不可能的(比如吃完一只烤羊腿),或者遇到了意外情况(吃到苍蝇 / 发生车祸 / 咖啡倒书上了……),我们也可能会让循环提前中止。
即便是头驴子,在发现无论如何都吃不到吊在面前的胡萝卜时,它也会放弃。但是电脑就没有这么聪明了,程序中的循环条件再怎么不合理它也会乖乖地执行,并会不知疲倦地一直运行下去。如果循环条件永远成立,循环就不会终止,就会形成死循环(Infinite Loop)。
死循环(Infinite Loop):因循环条件永远成立而不会停止运行的循环
“死循环”听上去是个可怕的概念。事实上,由于程序设计漏洞意外造成的死循环,可能会让电脑卡顿甚至死机。好在现在大多数操作系统都能处理这种情况,在这种情况下会友好地提示我们终止程序:
Paste_Image.png循环执行时都做些什么?
Paste_Image.png吃饭时我们重复的动作是什么呢?吃一口菜,吃一口饭,嚼一嚼,咽下去……这是在吃饭过程中要重复进行的流程,我们称之为循环体(Statement)。
循环体(Statement):每次循环时所执行的流程
循环体可以是空的。比如在睡觉的时候,我们什么也不做。又比如在烧开水的过程中,我们仅仅是在等待而已。
对一开始就给定次数的循环(比如做俯卧撑),我们需要在每次循环时更新计数,这样才能知道什么时候结束循环。
对遍历一个集合的循环(比如洗一盘子草莓),我们可能需要不断减少集合里的成员(比如把洗好的草莓放到另一个盘子里),又或者给处理过的成员做标记以区分(比如把洗好的草莓叶子都摘掉)。
在一次循环中,循环体到底重复执行多少次,我们可能知道(比如做俯卧撑),也可能不知道(比如吃饭),也可能要做完了才知道(比如洗草莓)。
怎么画一个方块?
Paste_Image.png现在回到我们的主题:我们要画一个方块。但由于不能使用fillRect的后两个参数,所以我们现在只能画出一个点,除了写一大堆画点语句之外,目前我们对用其他方法来完成这个任务还没有任何头绪。
是时候祭出杀手锏了,让我们来对这个任务进行拆分。
Paste_Image.png我们知道,在屏幕上的一个方块是由一大堆点构成的。这些点可以视为紧紧排在一起的一堆线,可以是一堆垂直方向的线,也可以是一堆水平方向的线。所以我们可以把任务拆分为画出一堆线。具体的流程是:
- 画出一条线
- 知道下一条线画在哪里
- 重复以上两步,在画够数量之后停下来
线则是由同一方向的点构成的,所以我们可以继续拆分为画出一堆点。具体的流程是:
- 画一个点
- 知道下一个点画在哪里
- 重复以上两步,在画够数量之后停下来
画一个点我们已经会了;下一个点的坐标位置我们也可以通过简单的加减计算得出;需要画的数量就是方块的尺寸。于是我们只要完成“重复以上两步”的功能,就可以画出一条线,进而画出方块了。
那这个“重复以上两步”怎么完成?这就需要用到我们刚学过的循环了。
初见循环
首先请在电脑上的Chrome浏览器中打开 http://codepen.io/zhangshenjia/pen/JWeWON,你会看到这样的界面:
Paste_Image.png我们可以看到,画布上有两条水平线。再看看代码:调缩放比例、调颜色、画点……这些都是我们上节课玩过的东西。在最后面有三行代码是我们没见过的:
Paste_Image.png这是JS里的一个for循环。
除了for循环外,在JS语言中还有while、do/while、for in循环,感兴趣的朋友可以查看相关的文档:http://www.w3school.com.cn/js/js_loop_for.asp
在 { 和 } 之前的代码,就是这个循环的循环体。细心的同学可能发现了,这行代码前面多加了两个空格,这是为了表现代码层次关系而添加的缩进。行首的空格对程序的运行来说没有任何实际影响,但能让人更容易理解代码的结构。
有的程序员喜欢在行首增加两个空格,有的程序员喜欢增加四个空格,还有的程序员喜欢在行首添加TAB制表符……孰优孰劣,难有定论。但可以确定的是,在一份代码里应该始终使用相同的缩进风格。
等下,这行语句好像很眼熟……这不就是我们用过的画点语句吗?没错,就是它。但是我们仔细看看,就会发现有点区别:第一个数字变成字母 i 了。
可能有的同学在上节课改代码时曾经试过(什么,你没试过?现在试试看!),这里必须写数字,如果改成字母,整个程序就会出问题的。比如我们把第一行画点语句里第一个数字改成字母 a 看看:
Paste_Image.png我的乖乖,整个画布都空了,这说明程序出问题了,赶紧改回来吧。那循环里的画点语句为什么就能这么写呢?我们可以把它改成数字 0 看看:
Paste_Image.png这下可好,第二条线变成一个点了,为啥呢?想想就会知道,每次执行循环的时候,都在0, 10这个固定的坐标位置上画点,结果可不就重到一起了嘛!我们可以看看前面的那堆画点语句(它们画出了画布最上方的那条线),可以看到第一个数字一直在发生变化:
Paste_Image.png把我们刚才的修改还原一下,第二条线就又出现了。事实上,每次循环时,这个 i 都在发生变化,它是一个变量(Variable)。
变量(Variable):可以用来保存和访问数据的具名地址
变量可以理解为我们做菜时,用来盛装食材的盘子。一开始所有的盘子都是空的,在准备炒菜的过程中,我们会把食材、调料等盛到不同的盘子里,然后按需取用。同一个盘子可能一会用来存放切好的蒜末,一会又用来盛拌好的凉菜……
做一顿饭可能会用到很多盘子,我们需要记住其中某些盘子的作用,比如这个盘子曾经装过生肉,那就不能用来装熟食。在多人协作的厨房里,盘子可能超级多,单纯靠个人的记忆就不太好使了,这时候就需要给盘子夹上标签来做记号(吃过麻辣香锅或羊肉泡馍的同学应该都见过)。
同样,程序里也可以有很多变量,所以我们也必须给变量进行命名,这样才能通过名字来使用它,也能通过名字得知变量的用途。这个循环变量的变量名就是 i。
为什么循环里的变量的名字要叫作 i 呢?这是个有趣的问题,你可以在课后搜索研究一下。
我们把每次需要画点的水平坐标存放在变量 i 里,需要画点的时候再把水平坐标从变量 i 里读取出来使用。在每次循环时,我们都改变它的值,这样就可以画出一条线来了。
改改改!
接下来,让我们通过试探性地修改,来了解for循环中各部分的作用。首先,把for循环中出现的第二个数字 5 改成 10,看看会发生什么:
Paste_Image.png可以看到第二条线延长了一倍。本来想要实现这样的效果,我们是需要多写5条画点语句的。而现在只需要改一个数字就行了,有没有很爽?看来这个数字决定了线条的长度,也就是循环执行多少次。
事实上,i < 10 就是循环的循环条件。在每次执行循环体前,都会对它进行判断,只有在循环条件成立的情况下,循环体才会被执行;如果不成立,循环就会结束。
要是把循环条件删掉,或者改成 i > 0 会怎么样?如果没有循环条件,或者循环条件一直都满足,就会死循环噢!你可以试试看(试完了记得把代码改回来)……放心,Chrome浏览器会检查出死循环并终止它,不会死机的。
我们再把第一个数字 0 改成 5 试试看:
Paste_Image.png可以看到第二条线变短且向右移动了。这个数字决定了我们第一个点从哪里开始画。如果你把这个数字改成一个很大的数,比如说 100,你会发现一个点都没有画出来,因为循环条件 i < 10 得不到满足,循环一次都不会被执行。
事实上,var i = 5是循环的初始化语句,它只在循环一开始执行一次,声明了一个变量 i 并给它赋一个初始值。这就相当于我们从橱柜拿出来一个盘子洗干净,装了点东西。
要是把初始化语句删掉,会怎么样呢?由于变量 i 没有声明,在判断循环条件时程序就会报错,循环根本就不会被执行。
让我们来研究下括号里的最后一部分,先把最后的数字 1 改成 2 看看:
Paste_Image.png恩,怎么变成虚线了?这个数字决定了下一个点画在哪里。你可以通过修改这个数字来调整两个点之间的距离(还可以是小数噢)。
事实上,* i = i + 1* 是循环的递增语句,它在每次循环体执行结束后都会被执行。一般情况下,递增语句主要的任务就是对循环变量进行更新,确保循环不会变成死循环。
i = i + 1 还可以写成 i += 1 和 **i++ **,感兴趣的朋友可以看看JS运算符的文档:http://www.w3school.com.cn/js/js_operators.asp
for循环是按照什么流程执行的?
Paste_Image.png- 先执行初始化语句;
- 判断循环条件,如果条件不满足,则退出循环;
- 如果条件满足,则执行循环体;
- 执行递增语句,然后跳到第二步。
我们可以在脑中模拟执行一下我们的代码,看看循环是怎么工作的:
循环刚开始时 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)。
Paste_Image.png嵌套(Nesting):在一个结构体内部包含另一个结构体
我们可以通过嵌套循环来解决多维度上的问题,每层循环处理一个维度上的变化,将问题“降维”之后在循环体内进一步解决。在我们今天的例子里,我们要在二维平面上画一个方块,就可以通过两层嵌套循环来实现,内层在水平方向循环(画点成线),外层在垂直方向循环(画线成面)。现在实现画点成线功能的内层循环我们已经有了,我们需要在它外面再写一个外层循环。
首先,请刷新一下页面,把代码恢复成原样,然后把第8-12行的画点语句删掉。这样屏幕上就只剩下用for循环画的那条线了。在for语句前面插入一个空行,然后照猫画虎写一条for语句,然后在循环后面插入一个空行,输入 } :
Paste_Image.png注意:在程序中出现的所有符号都是半角符号(如(;){),推荐在编程时关闭中文输入法,避免不小心输入全角符号(如(;){),导致语法错误。
现在代码的层次结构并不是很清楚,让我们选中内层循环的那三行,按一下 TAB 键,给它们增加缩进,这样看上去就容易理解多了:
Paste_Image.png等等,怎么画出来的还是条直线呢?因为我们循环里的画点语句只更改了x坐标,y坐标一直都是10。现在我们把画点语句里的第二个数字 10 改成 i 看看:
Paste_Image.png这下我们画出了一条斜线,还不是方块,这是为什么呢?想想就能明白,水平和垂直方向坐标相同,画出来的点就是(1, 1)、(2、2)、……连起来可不就是一条斜线嘛!我们想让水平和垂直坐标分别进行变化,只用一个变量 i 肯定是不行的,需要再增加一个变量。让我们把内层循环里的 i 改成 j,然后再把画点语句里的第二个 i 也改成 j:
Paste_Image.png耶,我们的方块终于画出来了!
为什么第二层循环的变量要取名为 j 呢?这也是一个惯例,如果有多层循环,会从 i 开始按顺序使用字母作为循环变量。
通过输出日志跟踪程序的运行情况
Paste_Image.png代码的结构层级越多,就越难搞明白它是怎么运作的。如果只有一层循环,还可以通过纯粹的思考来模拟,毕竟只有一个循环变量在发生变化。但在多层嵌套循环里这样做就难多了,因为你需要记住每一层循环的当前循环次数以及对应循环变量的当前值。
另外,随着嵌套循环层级的增多,最内部循环体的运行次数是呈指数增长的。像我们刚才仅仅画一个5x5的方块,内部的画点语句就执行了25次。想要跟踪每一步运行的情况不是不可以,而是太繁琐了。
幸好,我们有其他方法可以用来跟踪复杂程序的运行过程,那就是在控制台输出日志。
控制台(Console):一个交互界面,可以用来显示程序运行中的错误信息和调试打印日志
日志(Log):在程序运行过程中,按照指定的需求和格式留下的数据记录
让我们先在循环里的画点语句后面新增一行,把下面的代码复制过去:
Paste_Image.pngconsole.log('i=' + i + ', j=' + j);
这行代码的作用是,在每次执行循环体时,在控制台把循环变量 i 和 j 的值打印出来。接下来我们就要打开控制台,看看输出的日志:
首先按下 F12(Mac电脑按下Command + Option + I),或者在网页的任何位置点击鼠标右键,然后选择最下面的“检查”项,打开开发人员工具:
Paste_Image.png有可能你的开发人员工具出现在浏览器下面,或者是一个独立的窗口。你可以在界面的右上角的关闭按钮旁边找到设置功能,选择第三个布局,这样它就会固定在浏览器的右边了。
Paste_Image.png开发人员工具默认显示的是 Elements 选项卡,这是用来显示网页元素结构的,我们现在用不着。请切换到如图所示的 Console 选项卡,就能看到一堆我们输出的日志了:
Paste_Image.png通过跟踪日志输出,我们就可以知道每一次循环时,变量 i 和 j 分别的变化情况。有同学问:这里面的日志太多了,分不清哪些是之前输出的,哪些是这次输出的,该怎么办呢?
Paste_Image.png好办,我们可以点击左上角的清除按钮把所有的日志都干掉,再对程序进行一次无所谓的改动(比如在一个空行里加个空格)让它重新运行一次,这样新出现的日志就都是本次运行输出的了。
除了显示错误信息和记录调试日志外,控制台还可以直接输入JS代码进行执行,有兴趣的朋友可以自己研究体验一下。
内容回顾
Paste_Image.png本节课所学的概念:
循环(Loop):一段在程序中只出现一次,但可能会连续运行多次的代码。
循环条件(Condition):让循环继续重复执行的条件
循环体(Statement):每次循环时所执行的流程
死循环(Infinite Loop):因循环条件永远成立而不会停止运行的循环
变量(Variable):可以用来保存和访问数据的具名地址
嵌套(Nesting):在一个结构体内部包含另一个结构体
控制台(Console):一个交互界面,可以用来显示程序运行中的错误信息和调试打印日志
日志(Log):在程序运行过程中,按照指定的需求和格式留下的数据记录
本节课所学的原则:
Don’t Repeat Yourself:不要重复你自己
课后作业
Paste_Image.png1、找出至少一个在生活中循环的案例,并明确循环条件和循环体,比如:
循环:吃饭
循环条件:没吃饱
循环体:吃一口
2、配合使用循环和画点语句,画出更复杂的图案。
网友评论