在 JavaScript 编程中,代码的执行并非总是从上到下、逐行顺序进行。控制流语句(Control Flow Statements)赋予了开发者决定程序执行路径的能力,使其能够根据特定条件选择不同的代码块执行,或者重复执行某段代码,从而实现复杂而动态的逻辑。正是这些语句,让我们的程序能够“思考”并响应各种输入和状态变化。

本教程将全面深入地探讨 JavaScript 中各种控制流语句。我们将从基本的条件判断 (if-elseswitch) 开始,逐步过渡到强大的循环结构 (forwhiledo-whilefor-infor-of),并详细讲解如何使用 breakcontinue 来精确控制循环行为。此外,我们还将覆盖至关重要的错误处理机制 (try-catch-finallythrow),它确保了程序在面对异常情况时能够优雅地运行。通过理解这些语句的工作原理、最佳实践和常见陷阱,您将能够编写出更健壮、更高效、更具逻辑性的 JavaScript 代码。


1. 控制流的本质:程序执行的决策者

控制流,顾名思义,是程序执行指令的顺序。在没有任何控制流语句的情况下,程序会严格按照代码的顺序一步步向下执行。而控制流语句则允许我们:

  • 条件执行: 基于某些条件,决定是否执行某段代码(例如 if-else, switch)。
  • 重复执行: 重复执行某段代码直到满足某个条件(例如 for, while, do-while)。
  • 改变流程: 跳出当前执行的块或跳到下一个迭代(例如 break, continue)。
  • 异常处理: 优雅地处理程序运行时可能出现的错误(例如 try-catch-finally)。

掌握这些工具,是编写任何有意义的 JavaScript 应用程序的基石。


2. 条件语句:代码的“如果-那么”

条件语句允许程序根据条件的真假来选择性地执行代码块。

2.1 if...else if...else 语句

这是最基本的条件判断结构,用于根据一个或多个条件来执行不同的代码块。

  • 基本结构:
    if (condition1) {
        // 当 condition1 为 true 时执行的代码
    } else if (condition2) {
        // 当 condition1 为 false 且 condition2 为 true 时执行的代码
    } else {
        // 当所有条件都为 false 时执行的代码
    }
    
  • 条件中的 Truthy/Falsy 值:
    • condition 会被隐式转换为布尔值。false, 0, "" (空字符串), null, undefined, NaN 都是 Falsy 值,其余都是 Truthy 值。
    let score = 75;
    if (score >= 90) {
        console.log("优秀");
    } else if (score >= 60) {
        console.log("及格");
    } else {
        console.log("不及格");
    }
    
    let username = "";
    if (username) { // username 为 Falsy 值,条件为 false
        console.log(`Hello, ${username}!`);
    } else {
        console.log("请登录。"); // 执行这里
    }
    
  • 最佳实践:
    • 始终使用花括号 {} 包裹代码块,即使只有一行代码,这能避免潜在的逻辑错误和提高代码可读性。
    • 尽量避免过深的 if-else 嵌套,可以考虑使用函数封装、提前返回(Early Exit)或 switch 语句来简化。
2.2 switch 语句

switch 语句用于根据一个表达式的值来执行不同的代码块。它通常是处理多个分支条件的更简洁和易读的方式,特别是当所有分支都基于同一个变量的不同值时。

  • 基本结构:
    switch (expression) {
        case value1:
            // 当 expression 等于 value1 时执行
            // (使用 === 严格比较)
            break; // 阻止“穿透”到下一个 case
        case value2:
            // 当 expression 等于 value2 时执行
            break;
        default:
            // 当 expression 不匹配任何 case 时执行
    }
    
  • break 关键字:
    • break 至关重要,它会终止 switch 语句的执行,跳出 switch 代码块。
    • 如果省略 break,则代码会继续执行到下一个 case(称为“穿透”或“Fall-through”),这在某些特定场景下有用,但通常是导致 Bug 的原因。
  • default 关键字:
    • default 块是可选的,当 expression 的值不匹配任何 case 时,default 块中的代码会被执行。
  • 严格比较 (===):
    • switch 语句在比较 expressioncase 值时,会使用严格相等 (===)。这意味着它会同时比较值和类型,不会进行隐式类型转换。
    let day = 3;
    let dayName;
    switch (day) {
        case 1:
            dayName = "星期一";
            break;
        case 2:
            dayName = "星期二";
            break;
        case 3:
            dayName = "星期三"; // 匹配并执行
            break;              // 终止
        default:
            dayName = "未知";
    }
    console.log(dayName); // "星期三"
    
    let value = "1";
    switch (value) {
        case 1:
            console.log("数字 1");
            break;
        case "1":
            console.log("字符串 '1'"); // 匹配并执行
            break;
        default:
            console.log("不匹配");
    } // 输出: "字符串 '1'"
    
  • 最佳实践:
    • 每个 case 后都应该有 break,除非你是故意利用“穿透”行为(此时应添加注释说明)。
    • 几乎总是包含 default 块,用于处理未预期的值,提高程序的健壮性。
    • switch 语句的 case 值可以是表达式,但在实际使用中,通常是常量或字面量。
2.3 三元运算符 (Ternary Operator / Conditional Operator)

对于简单的 if-else 赋值或返回,三元运算符提供了一种更简洁的表达式形式。

  • 语法: condition ? expressionIfTrue : expressionIfFalse
    let age = 20;
    let status = (age >= 18) ? "成年人" : "未成年人";
    console.log(status); // "成年人"
    
    // 等同于:
    // let status;
    // if (age >= 18) {
    //   status = "成年人";
    // } else {
    //   status = "未成年人";
    // }
    
  • 最佳实践: 仅用于简单的条件赋值或表达式。避免嵌套三元运算符,以免降低可读性。

3. 循环语句:重复的力量

循环语句允许我们重复执行一段代码,直到满足特定条件。

3.1 for 循环

for 循环是最常用的循环结构之一,适用于需要明确指定迭代次数的场景。

  • 基本结构: for (initialization; condition; increment/decrement) { // 代码块 }
    • initialization (初始化):在循环开始前执行一次,通常用于声明和初始化循环计数器。
    • condition (条件):每次迭代前评估。如果为 true,则循环继续;如果为 false,则循环终止。
    • increment/decrement (增量/减量):每次迭代结束后执行,通常用于更新计数器。
    for (let i = 0; i < 5; i++) {
        console.log(`for 循环计数: ${i}`);
    }
    // 输出: 0, 1, 2, 3, 4
    
  • 无限循环: 如果 condition 永远为 true,则会创建无限循环,导致程序卡死。
    // for (;;) { // 慎用!无限循环示例
    //   console.log("永远运行");
    // }
    
  • 最佳实践:
    • 使用 let 声明循环变量 i,以确保其作用域仅限于循环块,避免全局污染和意外修改。
    • 对于遍历数组,现代 JavaScript 更推荐使用 for...of 或数组方法(如 forEach)。
3.2 while 循环

while 循环在每次迭代前检查条件,只有当条件为 true 时才执行循环体。

  • 基本结构: while (condition) { // 代码块 }
    • 在进入循环体之前,首先评估 condition
    • 如果 condition 第一次就为 false,则循环体将不会被执行。
    let count = 0;
    while (count < 3) {
        console.log(`while 循环计数: ${count}`);
        count++; // 确保循环终止
    }
    // 输出: 0, 1, 2
    
  • 最佳实践:
    • 务必确保循环体内部有某种机制能改变 condition,使其最终变为 false,否则将导致无限循环。
    • 适用于当循环次数不确定,但已知某个条件何时不再满足的场景。
3.3 do...while 循环

do...while 循环与 while 循环类似,但它至少会执行一次循环体,因为条件检查是在每次迭代结束时进行的。

  • 基本结构: do { // 代码块 } while (condition);
    let i = 0;
    do {
        console.log(`do-while 循环计数: ${i}`);
        i++;
    } while (i < 0); // 条件为 false,但循环体已经执行了一次
    // 输出: 0
    
  • 最佳实践:
    • 适用于需要确保循环体至少执行一次的场景,例如用户输入验证(即使第一次输入无效,也需要提示用户重新输入)。
3.4 for...in 循环

for...in 循环用于遍历对象的所有可枚举属性(包括原型链上的属性)。

  • 基本结构: for (variable in object) { // 代码块 }
    const person = {
        name: "Alice",
        age: 30,
        city: "New York"
    };
    
    for (let key in person) {
        console.log(`${key}: ${person[key]}`);
    }
    // 输出:
    // name: Alice
    // age: 30
    // city: New York
    
  • 重要警示: for...in 会遍历对象自身以及原型链上的所有可枚举属性。这在遍历数组时尤其危险,因为它会返回数组的索引(字符串),甚至可能返回不相关的原型属性。
    const arr = [1, 2, 3];
    arr.myProp = "hello"; // 添加自定义属性
    for (let index in arr) {
        console.log(index); // "0", "1", "2", "myProp"
    }
    
  • 最佳实践:
    • 不要用 for...in 遍历数组
    • 当遍历普通对象的属性时,应配合 hasOwnProperty() 方法来过滤掉继承的属性。
    for (let key in person) {
        if (person.hasOwnProperty(key)) {
            console.log(`${key}: ${person[key]}`);
        }
    }
    
3.5 for...of 循环 (ES6+)

for...of 循环用于遍历可迭代对象(如数组 Array、字符串 String、映射 Map、集合 SetNodeList 等)的元素值。

  • 基本结构: for (variable of iterable) { // 代码块 }
    const colors = ["red", "green", "blue"];
    for (let color of colors) {
        console.log(color);
    }
    // 输出:
    // red
    // green
    // blue
    
    const str = "hello";
    for (let char of str) {
        console.log(char);
    }
    // 输出:
    // h
    // e
    // l
    // l
    // o
    
  • 优势:
    • 直接访问元素的值,而不是索引或属性名。
    • 是遍历数组和字符串等可迭代对象的推荐方式,因为它简洁、安全,且不会遍历原型链上的属性。
  • 最佳实践:
    • 这是遍历数组元素、字符串字符、Map 和 Set 值的现代 JavaScript 最佳方式。
    • 如果要同时获取索引,可以使用 for (let [index, value] of array.entries()) { ... }

4. 循环控制语句:精准操作循环流程

在循环内部,我们可以使用特定的语句来更精细地控制循环的执行。

4.1 break 语句

break 语句用于立即终止当前所在的循环 (for, while, do-while) 或 switch 语句,并继续执行紧接着该循环/switch 的代码。

for (let i = 0; i < 10; i++) {
    if (i === 5) {
        console.log("遇到 5,跳出循环。");
        break; // 终止整个 for 循环
    }
    console.log(`当前数字: ${i}`);
}
console.log("循环已结束。");
// 输出: 0, 1, 2, 3, 4, 遇到 5,跳出循环。, 循环已结束。
  • 带标签的 break 用于跳出多重嵌套循环中的特定外部循环。
    outerLoop: for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            if (i === 1 && j === 1) {
                console.log("跳出外层循环");
                break outerLoop; // 跳出名为 outerLoop 的循环
            }
            console.log(`i: ${i}, j: ${j}`);
        }
    }
    console.log("所有循环都已结束。");
    
4.2 continue 语句

continue 语句用于跳过当前循环迭代中剩余的代码,并立即进入下一次迭代。

for (let i = 0; i < 5; i++) {
    if (i % 2 === 0) { // 如果是偶数
        console.log(`跳过偶数: ${i}`);
        continue; // 跳过当前迭代剩余部分,进入下一次迭代
    }
    console.log(`处理奇数: ${i}`);
}
// 输出:
// 跳过偶数: 0
// 处理奇数: 1
// 跳过偶数: 2
// 处理奇数: 3
// 跳过偶数: 4
  • 带标签的 continue 用于跳过多重嵌套循环中特定外部循环的当前迭代,进入其下一次迭代。

5. 错误处理:try-catch-finallythrow

错误处理是控制流的重要组成部分,它允许程序在发生意外情况(异常)时不会崩溃,而是优雅地捕获并处理这些错误。

5.1 try...catch 语句

try...catch 块用于测试一段代码中可能出现的错误,并在发生错误时捕获并处理它。

  • 基本结构:
    try {
        // 尝试执行可能抛出错误的代码
        let result = someUndefinedVariable * 2; // 这会抛出 ReferenceError
        console.log("这行代码不会执行");
    } catch (error) {
        // 捕获并处理错误
        console.error("发生错误:", error.message);
        console.error("错误对象:", error);
        // 通常在这里进行错误日志记录、用户友好的错误提示等
    }
    console.log("程序继续执行。");
    
  • error 对象:
    • catch 块会接收到一个 error 对象作为参数,其中包含有关错误的信息,例如 name (错误类型,如 ReferenceError, TypeError) 和 message (错误描述)。
  • 最佳实践:
    • 不要滥用 try-catch。只在真正可能出错且需要特定处理的地方使用。
    • catch 块中提供有意义的错误处理逻辑,而不是简单地 console.log。考虑用户体验、日志记录和错误上报。
5.2 finally

finally 块是可选的,它包含无论 try 块是否抛出错误、错误是否被 catch 块处理,都一定会执行的代码。

  • 基本结构:
    try {
        console.log("尝试执行代码...");
        // let value = JSON.parse("无效的 JSON 字符串"); // 抛出 SyntaxError
        console.log("try 块结束。");
    } catch (error) {
        console.error("在 catch 中处理错误:", error.message);
    } finally {
        // 无论如何都会执行的代码,通常用于资源清理
        console.log("finally 块执行。");
    }
    console.log("程序继续。");
    
  • 用途: 适用于在 try 块执行后需要进行清理工作的场景,例如关闭文件、释放网络连接、清除定时器等。
5.3 throw 语句

throw 语句用于显式地创建一个错误(抛出一个异常)。这允许开发者在检测到不符合预期的条件时,中断正常的程序流程,并通知调用者出现了问题。

  • 基本结构: throw value;
    • value 可以是任何值,但通常建议抛出 Error 对象的实例(如 new Error("错误消息")),因为它们提供了更丰富的错误信息(如堆栈跟踪)。
    function divide(a, b) {
        if (b === 0) {
            throw new Error("除数不能为零!"); // 抛出自定义错误
        }
        return a / b;
    }
    
    try {
        console.log(divide(10, 2)); // 5
        console.log(divide(10, 0)); // 抛出错误,接下来的代码不会执行
    } catch (error) {
        console.error("捕获到错误:", error.message); // 输出: 捕获到错误: 除数不能为零!
    }
    
  • 自定义错误: 可以通过继承 Error 类来创建自定义错误类型,使错误更具语义性。

6. 最佳实践与常见陷阱

  1. 始终使用 === 进行比较:if 条件和 switch case 中,避免使用 ==,以防止隐式类型转换导致的非预期行为。

  2. 避免无限循环: 确保 whiledo-while 循环的条件最终会变为 falsefor 循环的增量/减量逻辑正确。

  3. switch 语句添加 default 即使认为所有 case 都已覆盖,default 块也能处理意外情况,提高健壮性。

  4. for-of vs for-in

    • for-of 遍历数组、字符串、Map、Set 等可迭代对象的值,这是遍历集合的最佳实践。
    • for-in 遍历对象的可枚举属性名。在使用时,务必结合 hasOwnProperty() 方法过滤掉原型链上的属性。切勿用于遍历数组。
  5. 循环中的 let 声明:for 循环中使用 let 声明计数器变量,可以确保每次迭代都拥有一个独立的作用域,避免闭包陷阱(尤其是 setTimeout 等异步操作)。

  6. 错误处理要具体: catch 块不应该只是简单地 console.log(error)。应该根据错误类型进行不同的处理,或者将其转发到日志系统。

  7. 避免深层嵌套: 过多的 if-else 或循环嵌套会使代码难以阅读和维护。考虑使用函数抽象、提前返回(Guard Clauses)或重构逻辑。

  8. 提前返回 (Early Exit / Guard Clauses): 在函数开始处通过 if 语句处理无效输入或边缘情况,并直接 return,可以使主逻辑更清晰。

    // 悪い例: 深层嵌套
    function processUser(user) {
        if (user) {
            if (user.isActive) {
                if (user.hasPermission('edit')) {
                    // 实际处理逻辑
                } else {
                    // 没有权限
                }
            } else {
                // 用户未激活
            }
        } else {
            // 用户不存在
        }
    }
    
    // 好的例子: 提前返回
    function processUserBetter(user) {
        if (!user) {
            return console.error("用户不存在。");
        }
        if (!user.isActive) {
            return console.warn("用户未激活。");
        }
        if (!user.hasPermission('edit')) {
            return console.error("没有编辑权限。");
        }
        // 实际处理逻辑
    }
    

7. 总结与展望

JavaScript 控制流语句是构建任何动态和响应式应用程序的基石。它们是程序做出决策、重复任务和优雅处理意外情况的手段。

  • 条件语句 (if-else, switch, 三元运算符) 提供了基于条件的逻辑分支。
  • 循环语句 (for, while, do-while, for-in, `for-of) 实现了代码块的重复执行。
  • 循环控制 (break, `continue) 允许对循环流程进行精确的微调。
  • 错误处理 (try-catch-finally, `throw) 确保了程序在面对异常时能够稳定运行。

通过本教程的学习,您应该对 JavaScript 控制流语句有了全面的理解。请记住,编写清晰、高效且健壮的控制流代码,是成为一名优秀 JavaScript 开发者不可或缺的技能。不断实践,并根据项目需求灵活选择最合适的控制流结构,您的代码将因此而更加强大和可靠。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐