控制流:让条件与重复对应真实规则
计算表达式只能处理一步。程序需要根据条件选择路径,也需要重复动作。这里先用数字和字符串练习,避免还没学数组就被一排 map 劝退。
条件分支从最具体的情况开始
const score = 86;
let level;
if (score >= 90) {
level = '熟练';
} else if (score >= 60) {
level = '通过';
} else {
level = '待练习';
}
console.log(level); // 通过分支从上往下检查,命中一个就跳过后面的分支。若把 score >= 60 放在前面,96 分也只会落到“通过”,因为更具体的条件永远轮不到。条件有覆盖关系时,顺序就是业务规则的一部分。
条件位置会执行布尔转换。若拿到的是字符串 'false',if (value) 仍会进入。需要识别一个具体文本值时用 value === 'false',需要数量阈值时用 quantity > 0,别把所有判断都缩写成“有值”。
三元运算符产生一个值,适合简单二选一:
const remaining = 0;
const message = remaining > 0 ? '继续' : '结束';
console.log(message); // 结束两层以上的嵌套三元很容易读错。条件需要多个动作时,展开 if 比压成一行更易检查。
独立 if 不会自动变成互斥分支
const score = 96;
let label = '';
if (score >= 90) label = '优秀';
if (score >= 60) label = '通过';
console.log(label); // 通过第一句判断写入“优秀”,第二句判断也成立,随后覆盖为“通过”。程序没有“已经找到最合适的一项”的常识,它执行的是两次独立检查。
如果需求是选一个等级,用 if...else if 表达排他关系;如果需求是收集多项互不排斥的权限,两次 if 反而可能正确。判断结构应来自规则之间的关系,而不是按代码行数选择。
另一个常见错误是把数学的连写带进来:0 <= score <= 100 在JavaScript中先计算 0 <= score,得到布尔值,再拿它和100比较。写范围应使用 score >= 0 && score <= 100。这里 && 表示两项条件都要成立;|| 表示至少一项成立,! 把真假反转,下一篇会解释它们返回值与短路的完整规则。当score为150时,前一种写法仍可能为真,因为true转换为1,而1确实不大于100。
switch 比较具体值,不会自动转换类型
const command = 'next';
switch (command) {
case 'next':
case 'forward':
console.log('下一节');
break;
case 'back':
console.log('上一节');
break;
default:
console.log('未知指令');
}这里两个标签共用同一个分支,是有意的贯穿。没有 break 时,执行会继续落入后面的语句,直到退出 switch。case 1 不匹配字符串 '1',因为比较遵循严格相等语义。
case 标签自身不创建词法作用域。如果多个分支要声明同名 const,分别包一层 { ... }。不要在一个分支里声明变量,指望另一个分支一定执行过初始化。
循环由起点、条件、推进共同决定
let total = 0;
for (let day = 1; day <= 4; day += 1) {
total += day * 5;
}
console.log(total); // 50执行顺序是:初始化一次,检查条件,执行循环体,执行推进,再检查条件。循环体执行时 day 依次是 1、2、3、4。把 <= 改成 < 会少算一天,这就是典型的边界错误。
while 适合先检查条件再重复,do...while 会先执行一次:
let remaining = 3;
while (remaining > 0) {
console.log(remaining);
remaining -= 1;
}
let attempts = 0;
do {
attempts += 1;
} while (attempts < 0);
console.log(attempts); // 1检查一个循环是否会结束,找出一个向终点推进的量。remaining -= 1 若误写成 += 1,条件永远不会变假。不要真的运行无限循环来验证这一点。
用一条始终成立的描述检查循环
回到按天累计分钟的例子。每轮开始时,total 应当等于“已经处理完的那些天的分钟总数”。这条描述称为循环不变量,它帮助我们核对循环体是否漏算或重复计算。
| 本轮day | 进入本轮前total | 本轮加上 | 本轮结束后total |
|---|---|---|---|
| 1 | 0 | 5 | 5 |
| 2 | 5 | 10 | 15 |
| 3 | 15 | 15 | 30 |
| 4 | 30 | 20 | 50 |
处理完第4天后,推进使day成为5,条件失败;结果50因此恰好包含1~4天。若先推进day再累计,第一天就会被跳过;若循环结束后又手动加一次第4天,就会重复。
检查循环至少用三种输入:不需要执行的零次、刚好一次、正常多次。比如 for (let day = 1; day <= days; day += 1) 在days为0时一次也不执行,在1时只执行一次。只测4天,容易把边界错误藏在看似合理的总数里。
break 和 continue 只改变当前循环路径
let sum = 0;
for (let n = 1; n <= 10; n += 1) {
if (n % 2 === 0) continue;
if (n > 5) break;
sum += n;
}
console.log(sum); // 9,即 1 + 3 + 5n % 2 计算除以2的余数,余数为0表示偶数。
continue 跳过本轮剩余语句,for 仍会执行推进表达式;break 结束最近一层循环或 switch。while 的推进若放在 continue 后面,就可能永远执行不到,这是需要特别检查的区别。
标签可以让 break outer 退出指定的外层循环,也能让 continue outer 继续指定循环;合法但不常用。先确认能否把判断拆成函数,再考虑标签,避免出口分散到难以追踪。
for...of 遍历值,for...in 遍历可枚举字符串属性。现在记住两者目标不同即可;第三章结合数组与对象演示,不能把它们当作旧循环的任意替代品。
把 for 改成 while 时,推进位置不能丢
for 的 continue 会进入推进表达式,再检查条件;while 的 continue 直接回到条件。下面的while刻意把推进放在每轮开头,确保所有路径都能前进:
let n = 0;
let sum = 0;
while (n < 5) {
n += 1;
if (n % 2 === 0) continue;
sum += n;
}
console.log(sum); // 9如果把 n += 1 移到 continue 后面,到偶数时就会不断跳过推进。不是while不适合筛选,而是转换时没有保留控制路径。任何循环都应检查:正常路径、跳过路径、提前退出路径分别怎样改变状态。
把“状态”与“条件”分开命名
多个布尔值可能表达出不存在的组合,例如 isLoading 与 isDone 同时为真。状态互斥时,用一个明确字符串通常更容易验证。
let status = 'idle';
const action = 'start';
if (status === 'idle' && action === 'start') {
status = 'reading';
}
console.log(status); // reading这还不是状态机框架,只是在表达一条规则:“空闲时收到开始指令,才进入阅读中”。拿状态名、允许的动作和结果画一个小表,就能检查是否遗漏路径。不要先发明十个标志,再用条件拼命补洞。
练习
用循环计算 1 到 20 中能被 3 整除的整数之和,解释起止边界。
先约定输入是有限 Number(可能在区间外),把分数分为无效、未通过、通过、优秀四类:有效区间 0~100,通过线 60,优秀线 90。为什么应先处理无效值?
解释两个独立if为什么会把96分判成“通过”;把for改成while后,continue会跳到哪里,怎样保证仍然推进?