JavaScript从入门到精通 |循环
@[toc]
循环:while 和 for
我们经常需要重复执行一些操作。
例如,我们需要将列表中的商品逐个输出,或者运行相同的代码将数字 1 到 10 逐个输出。
循环 是一种重复运行同一代码的方法。
“while” 循环
while
循环的语法如下:
while (condition) {
// 代码
// 所谓的“循环体”
}
当 condition
为真时,执行循环体的 code
。
例如,以下将循环输出当 i < 3
时的 i
值:
let i = 0;
while (i < 3) { // 依次显示 0、1 和 2
alert( i );
i++;
}
循环体的单次执行叫作 一次迭代。上面示例中的循环进行了三次迭代。
如果上述示例中没有 i++
,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。
任何表达式或变量都可以是循环条件,而不仅仅是比较。在 while
中的循环条件会被计算,计算结果会被转化为布尔值。
例如,while (i != 0)
可简写为 while (i)
:
let i = 3;
while (i) { // 当 i 变成 0 时,条件为假,循环终止
alert( i );
i--;
}
ℹ️单行循环体不需要大括号
如果循环体只有一条语句,则可以省略大括号
{…}
:let i = 3; while (i) alert(i--);
“do…while” 循环
使用 do..while
语法可以将条件检查移至循环体 下面:
do {
// 循环体
} while (condition);
循环首先执行循环体,然后检查条件,当条件为真时,重复执行循环体。
例如:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);
这种形式的语法很少使用,除非你希望不管条件是否为真,循环体 至少执行一次。通常我们更倾向于使用另一个形式:while(…) {…}
。
“for” 循环
for
循环更加复杂,但它是最常使用的循环形式。
for
循环看起来就像这样:
for (begin; condition; step) {
// ……循环体……
}
我们通过示例来了解一下这三个部分的含义。下述循环从 i
等于 0
到 3
(但不包括 3
)运行 alert(i)
:
for (let i = 0; i < 3; i++) { // 结果为 0、1、2
alert(i);
}
我们逐个部分分析 for
循环:
语句段 | ||
---|---|---|
begin | let i = 0 |
进入循环时执行一次。 |
condition | i < 3 |
在每次循环迭代之前检查,如果为 false,停止循环。 |
body(循环体) | alert(i) |
条件为真时,重复运行。 |
step | i++ |
在每次循环体迭代后执行。 |
一般循环算法的工作原理如下:
开始运行
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ ...
所以,begin
执行一次,然后进行迭代:每次检查 condition
后,执行 body
和 step
。
如果你这是第一次接触循环,那么回到这个例子,在一张纸上重现它逐步运行的过程,可能会对你有所帮助。
以下是在这个示例中发生的事:
// for (let i = 0; i < 3; i++) alert(i)
// 开始
let i = 0
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// ……结束,因为现在 i == 3
ℹ️内联变量声明
这里“计数”变量
i
是在循环中声明的。这叫做“内联”变量声明。这样的变量只在循环中可见。for (let i = 0; i < 3; i++) { alert(i); // 0, 1, 2 } alert(i); // 错误,没有这个变量。
除了定义一个变量,我们也可以使用现有的变量:
let i = 0; for (i = 0; i < 3; i++) { // 使用现有的变量 alert(i); // 0, 1, 2 } alert(i); //3,可见,因为是在循环之外声明的
省略语句段
for
循环的任何语句段都可以被省略。
例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin
语句段。
就像这样:
let i = 0; // 我们已经声明了 i 并对它进行了赋值
for (; i < 3; i++) { // 不再需要 "begin" 语句段
alert( i ); // 0, 1, 2
}
我们也可以移除 step
语句段:
let i = 0;
for (; i < 3;) {
alert( i++ );
}
该循环与 while (i < 3)
等价。
实际上我们可以删除所有内容,从而创建一个无限循环:
for (;;) {
// 无限循环
}
请注意 for
的两个 ;
必须存在,否则会出现语法错误。
跳出循环
通常条件为假时,循环会终止。
但我们随时都可以使用 break
指令强制退出。
例如,下面这个循环要求用户输入一系列数字,在输入的内容不是数字时“终止”循环。
let sum = 0;
while (true) {
let value = +prompt("Enter a number", '');
if (!value) break; // (*)
sum += value;
}
alert( 'Sum: ' + sum );
如果用户输入空行或取消输入,在 (*)
行的 break
指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,alert
。
根据需要,"无限循环 + break
" 的组合非常适用于不必在循环开始/结束时检查条件,但需要在中间甚至是主体的多个位置进行条件检查的情况。
继续下一次迭代
continue
指令是 break
的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。
如果我们完成了当前的迭代,并且希望继续执行下一次迭代,我们就可以使用它。
下面这个循环使用 continue
来只输出奇数:
for (let i = 0; i < 10; i++) {
//如果为真,跳过循环体的剩余部分。
if (i % 2 == 0) continue;
alert(i); // 1,然后 3,5,7,9
}
对于偶数的 i
值,continue
指令会停止本次循环的继续执行,将控制权传递给下一次 for
循环的迭代(使用下一个数字)。因此 alert
仅被奇数值调用。
ℹ️**
continue
指令利于减少嵌套**显示奇数的循环可以像下面这样:
for (let i = 0; i < 10; i++) { if (i % 2) { alert( i ); } }
从技术角度看,它与上一个示例完全相同。当然,我们可以将代码包装在
if
块而不使用continue
。但在副作用方面,它多创建了一层嵌套(大括号内的
alert
调用)。如果if
中代码有多行,则可能会降低代码整体的可读性。
⚠️禁止 break/continue
在 ‘?’ 的右边
请注意非表达式的语法结构不能与三元运算符 ?
一起使用。特别是 break/continue
这样的指令是不允许这样使用的。
例如,我们使用如下代码:
if (i > 5) {
alert(i);
} else {
continue;
}
……用问号重写:
(i > 5) ? alert(i) : continue; // continue 不允许在这个位置
……代码会停止运行,并显示有语法错误。
这是不(建议)使用问号 ?
运算符替代 if
语句的另一个原因。
break/continue 标签
有时候我们需要一次从多层嵌套的循环中跳出来。
例如,下述代码中我们的循环使用了 i
和 j
,从 (0,0)
到 (3,3)
提示坐标 (i, j)
:
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let input = prompt(`Value at coords (${i},${j})`, '');
// 如果我想从这里退出并直接执行 alert('Done!')
}
}
alert('Done!');
我们需要提供一种方法,以在用户取消输入时来停止这个过程。
在 input
之后的普通 break
只会打破内部循环。这还不够 —— 标签可以实现这一功能!
标签 是在循环之前带有冒号的标识符:
labelName: for (...) {
...
}
break <labelName>
语句跳出循环至标签处:
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let input = prompt(`Value at coords (${i},${j})`, '');
// 如果是空字符串或被取消,则中断并跳出这两个循环。
if (!input) break outer; // (*)
// 用得到的值做些事……
}
}
alert('Done!');
上述代码中,break outer
向上寻找名为 outer
的标签并跳出当前循环。
因此,控制权直接从 (*)
转至 alert('Done!')
。
我们还可以将标签移至单独一行:
outer:
for (let i = 0; i < 3; i++) { ... }
continue
指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。
⚠️标签并不允许“跳到”所有位置
标签不允许我们跳到代码的任意位置。
例如,这样做是不可能的:
break label; // 跳转至下面的 label 处(无效)
label: for (...)
break
指令必须在代码块内。从技术上讲,任何被标记的代码块都有效,例如:
label: {
// ...
break label; // 有效
// ...
}
……尽管 99.9% 的情况下 break
都被用在循环内,就像在上面那些例子中我们看到的那样。
continue
只有在循环内部才可行。
总结
我们学习了三种循环:
while
—— 每次迭代之前都要检查条件。do..while
—— 每次迭代后都要检查条件。for (;;)
—— 每次迭代之前都要检查条件,可以使用其他设置。
通常使用 while(true)
来构造“无限”循环。这样的循环和其他循环一样,都可以通过 break
指令来终止。
如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么可以使用 continue
指令。
break/continue
支持循环前的标签。标签是 break/continue
跳出嵌套循环以转到外部的唯一方法。
✅任务
最后一次循环的值
重要程度: 3
此代码最后一次 alert 值是多少?为什么?
let i = 3;
while (i) {
alert( i-- );
}
解决方案
答案是:
1
。let i = 3; while (i) { alert( i-- ); }
每次循环迭代都将
i
减1
。当检查到i = 0
时,while(i)
循环停止。因此,此循环执行的步骤如下(“循环展开”):
let i = 3; alert(i--); // 显示 3,i 减至 2 alert(i--) // 显示 2,i 减至 1 alert(i--) // 显示 1,i 减至 0 // 完成,while(i) 检查循环条件并停止循环
while 循环显示哪些值?
重要程度: 4
对于每次循环,写出你认为会显示的值,然后与答案进行比较。
以下两个循环的 alert
值是否相同?
-
前缀形式
++i
:let i = 0; while (++i < 5) alert( i );
-
后缀形式
i++
let i = 0; while (i++ < 5) alert( i );
解决方案
这个题目展现了 i++/++i 两种形式在比较中导致的不同结果。
从 1 到 4
let i = 0; while (++i < 5) alert( i );
第一个值是
i = 1
,因为++i
首先递增i
然后返回新值。因此先比较1 < 5
然后通过alert
显示1
。然后按照
2, 3, 4…
—— 数值一个接着一个被显示出来。在比较中使用的都是递增后的值,因为++
在变量前。最终,
i = 4
时,在++i < 5
的比较中,i
值递增至5
,所以while(5 < 5)
不符合循环条件,循环停止。所以没有显示5
。从 1 到 5
let i = 0; while (i++ < 5) alert( i );
第一个值也是
i = 1
。后缀形式i++
递增i
然后返回 旧 值,因此比较i++ < 5
将使用i = 0
(与++i < 5
不同)。但
alert
调用是独立的。这是在递增和比较之后执行的另一条语句。因此它得到了当前的i = 1
。接下来是
2, 3,4…
我们在
i = 4
时暂停,前缀形式++i
会递增i
并在比较中使用新值5
。但我们这里是后缀形式i++
。因此,它将i
递增到5
,但返回旧值。因此实际比较的是while(4 < 5)
—— true,程序继续执行alert
。
i = 5
是最后一个值,因为下一步比较while(5 < 5)
为 false。
“for” 循环显示哪些值?
重要程度: 4
对于每次循环,写下它将显示的值。然后与答案进行比较。
两次循环 alert
值是否相同?
-
后缀形式:
for (let i = 0; i < 5; i++) alert( i );
-
前缀形式:
for (let i = 0; i < 5; ++i) alert( i );
解决方案
答案:在这两种情况下都是从
0
到4
。for (let i = 0; i < 5; ++i) alert( i ); for (let i = 0; i < 5; i++) alert( i );
这可以很容易地从
for
算法中推导出:
- 在一切开始之前执行
i = 0
。- 检查
i < 5
条件- 如果
true
—— 执行循环体并alert(i)
,然后进行i++
递增
i++
与检查条件(2)分开。这只是另一种写法。在这没使用返回的递增值,因此
i++
和++i
之间没有区别。
使用 for 循环输出偶数
重要程度: 5
使用 for
循环输出从 2
到 10
的偶数。
解决方案
for (let i = 2; i <= 10; i++) { if (i % 2 == 0) { alert( i ); } }
我们使用 “modulo” 运算符
%
来获取余数,并检查奇偶性。
用 “while” 替换 “for”
重要程度: 5
重写代码,在保证不改变其行为的情况下,将 for
循环更改为 while
(输出应保持不变)。
for (let i = 0; i < 3; i++) {
alert( `number ${i}!` );
}
解决方案
let i = 0; while (i < 3) { alert( `number ${i}!` ); i++; }
重复输入,直到正确为止
重要程度: 5
编写一个提示用户输入大于 100
的数字的循环。如果用户输入其他数值 —— 请他重新输入。
循环一直在请求一个数字,直到用户输入了一个大于 100
的数字、取消输入或输入了一个空行为止。
在这我们假设用户只会输入数字。在本题目中,不需要对非数值输入进行特殊处理。
解决方案
let num; do { num = prompt("Enter a number greater than 100?", 0); } while (num <= 100 && num);
两个检查都为真时,继续执行
do..while
循环:
- 检查
num <= 100
—— 即输入值仍然不大于100
。- 当
num
为null
或空字符串时,&& num
的结果为 false。那么while
循环也会停止。P.S. 如果
num
为null
,那么num <= 100
为true
。因此用户单击取消,如果没有第二次检查,循环就不会停止。两次检查都是必须的。
输出素数(prime)
重要程度: 3
大于 1
且不能被除了 1
和它本身以外的任何数整除的整数叫做[素数](素数是什么_百度知道 (baidu.com))。
换句话说,n > 1
且不能被 1
和 n
以外的任何数整除的整数,被称为素数。
例如,5
是素数,因为它不能被 2
、3
和 4
整除,会产生余数。
写一个可以输出 2
到 n
之间的所有素数的代码。
当 n = 10
,结果输出 2、3、5、7
。
P.S. 代码应适用于任何 n
,而不是对任何固定值进行硬性调整。
解决方案
这个题目有很多解法。
我们使用一个嵌套循环:
对于间隔中的每个 i { 检查在 1~i 之间,是否有 i 的除数 如果有 => 这个 i 不是素数 如果没有 => 这个 i 是素数,输出出来 }
使用标签的代码:
let n = 10; nextPrime: for (let i = 2; i <= n; i++) { // 对每个自然数 i for (let j = 2; j < i; j++) { // 寻找一个除数…… if (i % j == 0) continue nextPrime; // 不是素数,则继续检查下一个 } alert( i ); // 输出素数 }
这段代码有很大的优化空间。例如,我们可以从
2
到i
的平方根之间的数中寻找除数。无论怎样,如果我们想要在很大的数字范围内实现高效率,我们需要改变实现方法,依赖高等数学和复杂算法,如[二次筛选法(Quadratic sieve)](筛选法_百度百科 (baidu.com)),[普通数域筛选法(General number field sieve)](普通数域筛选法_百度百科 (baidu.com))等。译注:素数也称为质数,对本答案的代码进一步优化,其实就是一道 LeetCode 算法题,感兴趣的可以点击链接查看如何通过 [埃拉托斯特尼筛法筛选素数](埃拉托斯特尼筛法_百度百科 (baidu.com))。
“switch” 语句
switch
语句可以替代多个 if
判断。
switch
语句为多分支选择的情况提供了一个更具描述性的方式。
语法
switch
语句有至少一个 case
代码块和一个可选的 default
代码块。
就像这样:
switch(x) {
case 'value1': // if (x === 'value1')
...
[break]
case 'value2': // if (x === 'value2')
...
[break]
default:
...
[break]
}
- 比较
x
值与第一个case
(也就是value1
)是否严格相等,然后比较第二个case
(value2
)以此类推。 - 如果相等,
switch
语句就执行相应case
下的代码块,直到遇到最靠近的break
语句(或者直到switch
语句末尾)。 - 如果没有符合的 case,则执行
default
代码块(如果default
存在)。
举个例子
switch
的例子(高亮的部分是执行的 case
部分):
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Too small' );
break;
case 4:
alert( 'Exactly!' );
break;
case 5:
alert( 'Too big' );
break;
default:
alert( "I don't know such values" );
}
这里的 switch
从第一个 case
分支开始将 a
的值与 case
后的值进行比较,第一个 case
后的值为 3
匹配失败。
然后比较 4
。匹配,所以从 case 4
开始执行直到遇到最近的 break
。
如果没有 break
,程序将不经过任何检查就会继续执行下一个 case
。
无 break
的例子:
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Too small' );
case 4:
alert( 'Exactly!' );
case 5:
alert( 'Too big' );
default:
alert( "I don't know such values" );
}
在上面的例子中我们会看到连续执行的三个 alert
:
alert( 'Exactly!' );
alert( 'Too big' );
alert( "I don't know such values" );
ℹ️任何表达式都可以成为
switch/case
的参数
switch
和case
都允许任意表达式。比如:
let a = "1"; let b = 0; switch (+a) { case b + 1: alert("this runs, because +a is 1, exactly equals b+1"); break; default: alert("this doesn't run"); }
这里
+a
返回1
,这个值跟case
中b + 1
相比较,然后执行对应的代码。
“case” 分组
共享同一段代码的几个 case
分支可以被分为一组:
比如,如果我们想让 case 3
和 case 5
执行同样的代码:
let a = 3;
switch (a) {
case 4:
alert('Right!');
break;
case 3: // (*) 下面这两个 case 被分在一组
case 5:
alert('Wrong!');
alert("Why don't you take a math class?");
break;
default:
alert('The result is strange. Really.');
}
现在 3
和 5
都显示相同的信息。
switch/case
有通过 case 进行“分组”的能力,其实是 switch 语句没有 break
时的副作用。因为没有 break
,case 3
会从 (*)
行执行到 case 5
。
类型很关键
强调一下,这里的相等是严格相等。被比较的值必须是相同的类型才能进行匹配。
比如,我们来看下面的代码:
let arg = prompt("Enter a value?")
switch (arg) {
case '0':
case '1':
alert( 'One or zero' );
break;
case '2':
alert( 'Two' );
break;
case 3:
alert( 'Never executes!' );
break;
default:
alert( 'An unknown value' )
}
- 在
prompt
对话框输入0
、1
,第一个alert
弹出。 - 输入
2
,第二个alert
弹出。 - 但是输入
3
,因为prompt
的结果是字符串类型的"3"
,不严格相等===
于数字类型的3
,所以case 3
不会执行!因此case 3
部分是一段无效代码。所以会执行default
分支。
✅任务
将 “switch” 结构重写为 “if” 结构
重要程度:five:
将下面 switch
结构的代码写成 if..else
结构:
switch (browser) {
case 'Edge':
alert( "You've got the Edge!" );
break;
case 'Chrome':
case 'Firefox':
case 'Safari':
case 'Opera':
alert( 'Okay we support these browsers too' );
break;
default:
alert( 'We hope that this page looks ok!' );
}
解决方案
为了精确实现
switch
的功能,if
必须使用严格相等'==='
。对于给定的字符串,一个简单的
'=='
也可以。if(browser == 'Edge') { alert("You've got the Edge!"); } else if (browser == 'Chrome' || browser == 'Firefox' || browser == 'Safari' || browser == 'Opera') { alert( 'Okay we support these browsers too' ); } else { alert( 'We hope that this page looks ok!' ); }
请注意:将
browser == 'Chrome' || browser == 'Firefox' …
结构分成多行可读性更高。但
switch
结构更清晰明了。
将 “if” 结构重写为 “switch” 结构
重要程度:four:
用 switch
重写以下代码:
let a = +prompt('a?', '');
if (a == 0) {
alert( 0 );
}
if (a == 1) {
alert( 1 );
}
if (a == 2 || a == 3) {
alert( '2,3' );
}
解决方案
前两个检查为前两个
case
,第三个检查分为两种情况:let a = +prompt('a?', ''); switch (a) { case 0: alert( 0 ); break; case 1: alert( 1 ); break; case 2: case 3: alert( '2,3' ); break; }
请注意:最后的
break
不是必须的。但是为了让代码可扩展我们要把它加上。有可能之后我们想要再添加一个
case
,例如case 4
。如果我们忘记在它之前添加一个 break,那么在 case 3 执行结束后可能会出现错误。所以这是一种自我保险。
👉练习打卡
- 点赞
- 收藏
- 关注作者
评论(0)