控制流

让程序学会选择和重复

🎯 学习目标

学完本章,你将能够:

  • 能够使用 if/else if/else 和 switch 实现多条件分支判断
  • 能够选择合适的循环结构(for、while、do...while)处理重复任务
  • 能够使用 break 和 continue 控制循环流程,包括跳出多层循环
  • 能够使用 for...in 遍历对象属性,使用 for...of 遍历可迭代对象
  • 能够使用 try...catch 捕获和处理运行时错误

📌 有编程基础?if / for / while / switch 这些你都会。本章唯一需要留意的 JS 细节:for...in vs for...of——遍历对象用前者、遍历数组用后者,搞反会踩坑。建议快速浏览后,直接进入第 8 章。

你有没有想过

你在银行取钱时,如果余额不足,ATM 会拒绝并提示"余额不足"。如果密码错误三次,它会锁定卡片。这些"根据不同情况做不同事"的逻辑,是怎么实现的?

答案是控制流。程序默认从上到下逐行执行,但现实世界需要"如果……就……"和"重复做……直到……"。

控制流就是改变程序执行顺序的机制。有了它,程序才能做判断、做选择、做重复。没有控制流的程序,就像只会直走的汽车,遇到墙就撞上去。

这个比喻只是为了帮你建立直观感受。在计算机内部,控制流是通过 CPU 的条件跳转指令实现的,并不存在什么"汽车"和"墙"。

控制流预览

一张流程图看懂 ATM 取款逻辑

回看开头的 ATM 问题。它的完整逻辑其实就是一个复杂的控制流组合:

图:ATM 取款机的控制流流程图

这一章我们学完条件判断、循环和错误处理,你就能实现这样的逻辑了。

Part 1: 条件判断

程序最基本的能力:根据条件选择不同的执行路径

回顾开头的 ATM 例子:密码对不对、余额够不够,走的完全是不同的路。这就是条件判断——让程序学会"看情况办事"。先从最基础的 if...else 开始,再到多分支的 switch,每个语法都配有真实场景。学完这一节,你就能写出"如果……否则……"的逻辑了。

if 语句

如果满足条件,就执行这段代码

if 语句是最基本的条件判断结构。条件为 true 时执行花括号里的代码,为 false 时跳过。

说白了,就是"如果下雨就带伞"的逻辑。条件满足就做,不满足就跳过。

let age = 18;
if (age >= 18) {
    console.log("成年人");
}

// if...else——二选一
if (age >= 18) {
    console.log("成年人");
} else {
    console.log("未成年人");
}

else if 多条件

不只二选一,可以多选一

let score = 85;
if (score >= 90) {
    console.log("优秀");
} else if (score >= 80) {
    console.log("良好");     // 匹配这个
} else if (score >= 60) {
    console.log("及格");
} else {
    console.log("不及格");
}

// 嵌套 if——条件里还有条件
let isAdmin = true;
if (age >= 18) {
    if (isAdmin) {
        console.log("管理员用户");
    } else {
        console.log("普通用户");
    }
}

注意:else if 从上到下逐个判断,第一个匹配的条件执行后就不再往下看了。

条件判断陷阱

falsy 值可能让你的条件"失灵"

// 危险!0 是 falsy,条件为 false
let count = 0;
if (count) {
    console.log("有数据"); // 不会执行!
}

// 正确写法:明确判断
if (count !== 0) {
    console.log("有数据");
}

// 省略花括号——只对单行有效,但容易出 bug
if (score >= 60) console.log("及格");
// 不推荐!加个 else 就容易写错缩进
症状:if (count) 中 count 为 0 时条件不成立。
原因:0、""、null、undefined、NaN、false 都是 falsy,在条件中被视为 false。
修复:用 !== 0、!== "" 等明确判断,不要依赖隐式布尔转换。

switch 语句

一个值有很多种可能时,用 switch 更清晰

let day = "星期一";
switch (day) {
    case "星期一":
        console.log("工作日开始");
        break;
    case "星期五":
        console.log("快到周末了");
        break;
    case "星期六":
    case "星期日":           // case 穿透——两天都输出"周末"
        console.log("周末!");
        break;
    default:
        console.log("普通工作日");
        break;
}

三个注意点:

  • 别忘了 break,否则会穿透到下一个 case
  • switch 用 === 严格比较:"1" 不会匹配 1
  • default 放最后,处理所有未匹配的情况

switch 技巧

switch(true)——另一种多条件写法

// switch(true) 可以替代复杂的 else if
let score = 85;
switch (true) {
    case score >= 90:
        console.log("优秀");
        break;
    case score >= 80:
        console.log("良好");
        break;
    case score >= 60:
        console.log("及格");
        break;
    default:
        console.log("不及格");
}

// switch 用 === 严格匹配
let num = "1";
switch (num) {
    case 1:
        console.log("数字1"); // 不匹配!"1" !== 1
        break;
    case "1":
        console.log("字符串1"); // 匹配这个
        break;
}

试一试

练习一

任务:写一个程序,判断一个年份是否是闰年。规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。

let year = 2024;
// 你的代码:

参考答案

let year = 2024;
if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
    console.log(`${year} 是闰年`);
} else {
    console.log(`${year} 不是闰年`);
}

Part 2: 循环

重复执行一段代码,是程序最强大的能力之一

条件判断让程序能"选择",但还有一种更厉害的能力——"重复"。比如要处理 100 条数据,你不会写 100 遍同样的代码吧?for、while、for...of 就是用来搞定重复工作的。这一节把 JS 里常用的循环方式全部过一遍,告诉你什么时候该用哪一个。

for 循环

知道重复几次就用 for

for 循环由三部分组成:初始化、条件判断、更新。它是最常用的循环结构。

// 经典 for 循环
for (let i = 0; i < 5; i++) {
    console.log(i); // 0, 1, 2, 3, 4
}

// 倒序循环
for (let i = 5; i > 0; i--) {
    console.log(i); // 5, 4, 3, 2, 1
}

for 循环就像跑步:let i = 0 是起点,i < 5 是终点线,i++ 是每跑一步。

这个比喻只是为了帮你建立直观感受。在计算机内部,for 循环是通过条件寄存器反复比较和跳转来实现的。

for...in 与 for...of

遍历对象和遍历数组的两把利器

// for...in——遍历对象的键
let obj = { name: "张三", age: 25 };
for (let key in obj) {
    console.log(key, obj[key]);
}
// name 张三
// age 25

// for...of——遍历可迭代对象的值
let arr = [10, 20, 30];
for (let item of arr) {
    console.log(item); // 10, 20, 30
}

// for...of 还能遍历字符串
for (let char of "Hello") {
    console.log(char); // H, e, l, l, o
}

// 获取索引:entries()
for (let [index, value] of arr.entries()) {
    console.log(index, value);
}

while 循环

不知道重复几次就用 while

while 循环先判断条件,条件为 true 就执行循环体。适合不知道循环次数的场景。

// while —— 先判断后执行
let i = 0;
while (i < 5) {
    console.log(i);
    i++;
}

// 实际应用:读取用户输入直到满足条件
// let input;
// while (input !== "exit") {
//     input = getUserInput();
// }
症状:程序卡住不动,CPU 占用飙升。
原因:while 的条件永远为 true,形成了无限循环。通常是忘了更新循环变量。
修复:确保循环体中有让条件变为 false 的代码。

do...while

至少执行一次的循环

do...while 先执行一次循环体,再判断条件。适合"至少要做一次"的场景。

// 生成随机数,直到出现 7
let num;
do {
    num = Math.floor(Math.random() * 10);
    console.log("生成:", num);
} while (num !== 7);
console.log("终于生成了 7!");

while 和 do...while 的区别:while 可能一次都不执行,do...while 至少执行一次。就像"先看天气再决定出门"和"先出门再看天气"。

这个比喻只是为了帮你建立直观感受。在计算机内部,do...while 是先执行循环体代码,然后再通过条件跳转指令决定是否重复执行。

循环方式对比

一张表选对循环

循环适用场景特点
for已知次数的循环初始化、条件、更新一体
for...in遍历对象属性键包含原型链属性
for...of遍历可迭代对象的值数组、字符串、Map、Set
while不确定次数的循环先判断后执行
do...while至少执行一次先执行后判断
forEach遍历数组不能 break/continue

Part 3: 循环控制

循环不一定要跑到底,你可以在中途退出或跳过某一次

循环会写了,但有时你不需要跑完每一轮。比如找到目标就停,或者跳过不需要处理的数据。break 和 continue 就是干这个的——一个说"我不干了",一个说"这轮跳过"。还有嵌套循环怎么从里层直接跳出外层?这一节都给你讲清楚。

break 与 continue

中途退出和跳过本轮

// break——跳出整个循环
for (let i = 0; i < 10; i++) {
    if (i === 5) break;
    console.log(i); // 0, 1, 2, 3, 4
}

// continue——跳过当前这一轮,进入下一轮
for (let i = 0; i < 10; i++) {
    if (i % 2 === 0) continue;
    console.log(i); // 1, 3, 5, 7, 9(只输出奇数)
}

简单记忆:break 是"我不干了,走人",continue 是"这轮跳过,下一轮继续"。

这个比喻只是为了帮你建立直观感受。在计算机内部,break 是跳转到循环体结束之后的指令,continue 是跳转到循环体的条件判断处。

跳出多层循环

标签语句——给循环起个名字

// 标签语句——跳出外层循环
outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) {
            break outer; // 直接跳出外层循环
        }
        console.log(i, j);
    }
}
// 输出: (0,0) (0,1) (0,2) (1,0)

标签语句虽然合法,但会让代码难以阅读。实际开发中,推荐用函数封装 + return 替代。

// 推荐写法:用函数替代标签
function findTarget() {
    for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            if (i === 1 && j === 1) return;
            console.log(i, j);
        }
    }
}
findTarget();

试一试

练习二

任务:用 for 循环计算 1 到 100 的总和。

// 你的代码:

参考答案

let sum = 0;
for (let i = 1; i <= 100; i++) {
    sum += i;
}
console.log(sum); // 5050

练习三

任务:用 for...of 遍历数组 [3, 7, 2, 9, 1],只输出其中的偶数。

// 你的代码:

参考答案

let nums = [3, 7, 2, 9, 1];
for (let n of nums) {
    if (n % 2 !== 0) continue;
    console.log(n); // 2
}

Part 4: 错误处理

程序不可能永远不出错,学会优雅地处理错误比"不出错"更重要

条件判断和循环都搞定了,但还有一件事:代码跑着跑着出错了怎么办?用户传了个非法参数、文件读取失败、网络请求超时——这些都不是"如果"而是"何时"会发生的事。try...catch 让你的程序遇到错误时不崩溃,而是优雅地处理并继续运行。这是从"玩具代码"走向"生产代码"的关键一步。

try...catch

出了错也不崩溃,优雅地处理

try...catch 用于捕获代码运行时的错误。try 里放可能出错的代码,catch 里放出错后的处理逻辑。

try {
    let result = riskyOperation();
    console.log(result);
} catch (error) {
    console.error("出错了:", error.message);
}

// try...catch...finally
function readFile(path) {
    try {
        console.log("打开文件:", path);
        throw new Error("文件不存在"); // 模拟错误
    } catch (error) {
        console.error("错误:", error.message);
    } finally {
        console.log("无论如何都执行(清理资源)");
    }
}

错误处理陷阱

异步错误 catch 不到?这是个经典坑

// try...catch 只能捕获同步错误
try {
    setTimeout(() => {
        throw new Error("异步错误"); // 不会被捕获!
    }, 1000);
} catch (e) {
    console.log("捕获不到"); // 不会执行
}

// 正确方式:把 try...catch 放进异步回调里
setTimeout(() => {
    try {
        throw new Error("异步错误");
    } catch (e) {
        console.log("捕获到了:", e.message); // OK
    }
}, 1000);
症状:明明有 try...catch,但异步代码里的错误还是导致程序崩溃。
原因:try...catch 只能捕获同步代码中的错误。setTimeout 的回调在 try...catch 执行完毕后才运行。
修复:把 try...catch 放在异步回调内部,或使用 Promise 的 .catch()。

捕获特定错误

不同类型的错误,不同对待

// 用 instanceof 区分错误类型
try {
    JSON.parse("invalid json");
} catch (error) {
    if (error instanceof SyntaxError) {
        console.log("JSON 解析错误:", error.message);
    } else {
        throw error; // 不认识的错误,重新抛出
    }
}

// 常见错误类型
// SyntaxError  —— 语法错误(代码写错了)
// ReferenceError —— 引用了不存在的变量
// TypeError    —— 类型操作错误(对 null 调方法等)
// RangeError   —— 值超出范围

Error 对象

错误信息里藏着什么?

当 catch 捕获到错误时,参数是一个 Error 对象。它有三个核心属性:

try {
    let obj = null;
    obj.name; // TypeError!
} catch (error) {
    console.log(error.name);    // "TypeError"
    console.log(error.message); // "Cannot read properties of null"
    console.log(error.stack);   // 完整的调用栈(文件名+行号)
}

// 手动抛出错误——创建自定义错误信息
function divide(a, b) {
    if (b === 0) {
        throw new Error("除数不能为零");
    }
    return a / b;
}

try {
    divide(10, 0);
} catch (e) {
    console.error(e.message); // "除数不能为零"
}

error.stack 是调试最有价值的属性——它告诉你错误从哪一行开始、经过了哪些函数调用。在 VS Code 终端中,点击 stack 中的文件路径可以直接跳转到对应代码。

常见错误诊断

调试循环的第一步:在循环体内加 console.log,看每次迭代变量的值是否符合预期。

问题一:for 循环多执行或少执行了一次

症状:循环结果比预期多一个或少一个。

原因:条件写成了 <= 而不是 <,或者初始值不对。

修复:用小数字(比如 3)手动模拟一遍循环过程,确认边界。

问题二:switch 没有 break 导致穿透

症状:明明只匹配了一个 case,但输出了多个 case 的结果。

原因:忘记写 break,代码"穿透"到了下一个 case。

修复:每个 case 末尾加上 break。除非你有意利用穿透(比如多个 case 共享同一段逻辑)。

问题三:while 循环死循环

症状:程序卡住不动,浏览器标签页变灰或 Node.js 进程不退出。

原因:循环条件永远为 true,通常是忘了更新循环变量。

修复:确保循环体中有让条件变为 false 的代码。

综合练习

练习四:FizzBuzz

任务:经典编程面试题。遍历 1~30,遇到 3 的倍数输出 "Fizz",5 的倍数输出 "Buzz",同时是 3 和 5 的倍数输出 "FizzBuzz",其他数字原样输出。

1
2
Fizz
4
Buzz
Fizz
7
...
14
FizzBuzz
16

提示:先判断 15 的倍数(同时是 3 和 5 的倍数),再单独判断。

参考答案

for (let i = 1; i <= 30; i++) {
    if (i % 15 === 0) {
        console.log("FizzBuzz");
    } else if (i % 3 === 0) {
        console.log("Fizz");
    } else if (i % 5 === 0) {
        console.log("Buzz");
    } else {
        console.log(i);
    }
}

练习五:密码验证

任务:写一个密码验证程序,从命令行参数读取密码。规则:至少 8 位、包含大写字母、包含数字。不满足条件时用 throw 抛出错误。

$ node password.js "Abc123"
错误: 密码至少需要8位

$ node password.js "Abcdefg1"
密码强度:合格

提示:用 try...catch 包裹验证逻辑,用正则或循环检查字符类型。

参考答案

let password = process.argv[2];

try {
    if (password.length < 8) {
        throw new Error("密码至少需要8位");
    }
    if (!/[A-Z]/.test(password)) {
        throw new Error("密码需要包含大写字母");
    }
    if (!/[0-9]/.test(password)) {
        throw new Error("密码需要包含数字");
    }
    console.log("密码强度:合格");
} catch (e) {
    console.log("错误:", e.message);
}

📝 要点总结

本章核心知识点

条件分支

if/else 和 switch 实现多路径选择

循环结构

for、while、do...while 处理重复任务

循环控制

break 跳出循环,continue 跳过当前迭代

遍历方式

for...in 遍历对象,for...of 遍历可迭代对象

错误处理

try/catch 捕获异常,防止程序崩溃

下一章

现在你已经能让程序做判断、做重复了。下一章我们学习函数基础——把一段逻辑封装起来,随时调用、反复复用。

1 / X