====== 第三章 控制流 ====== ===== 3.1 程序控制流概述 ===== 程序控制流决定了代码执行的顺序。默认情况下,JavaScript代码从上到下顺序执行,但通过控制流语句,我们可以: * 根据条件选择执行不同的代码分支 * 重复执行某段代码 * 跳过某些代码或提前退出 控制流语句分为三类: * **条件语句**:if...else、switch * **循环语句**:while、do...while、for、for...in、for...of * **跳转语句**:break、continue、return、throw ===== 3.2 条件语句 ===== ==== 3.2.1 if语句 ==== 最基本的条件语句,根据条件决定是否执行代码块。 // 基本if语句 let age = 20; if (age >= 18) { console.log("已成年"); } // if...else语句 let score = 75; if (score >= 60) { console.log("及格"); } else { console.log("不及格"); } // if...else if...else语句 let grade = 85; if (grade >= 90) { console.log("优秀"); } else if (grade >= 80) { console.log("良好"); } else if (grade >= 70) { console.log("中等"); } else if (grade >= 60) { console.log("及格"); } else { console.log("不及格"); } **条件表达式求值**: // 条件会被转换为布尔值 if (0) { } // 不执行 if ("") { } // 不执行 if (null) { } // 不执行 if (undefined) { } // 不执行 if (NaN) { } // 不执行 if (false) { } // 不执行 if (1) { } // 执行 if ("hello") { } // 执行 if ({}) { } // 执行 if ([]) { } // 执行 ==== 3.2.2 嵌套if语句 ==== let isLoggedIn = true; let isAdmin = false; if (isLoggedIn) { console.log("欢迎回来"); if (isAdmin) { console.log("管理员权限"); } else { console.log("普通用户权限"); } } else { console.log("请先登录"); } // 更好的写法:使用逻辑运算符 if (isLoggedIn && isAdmin) { console.log("欢迎管理员"); } else if (isLoggedIn) { console.log("欢迎用户"); } else { console.log("请先登录"); } ==== 3.2.3 switch语句 ==== 当需要对同一个表达式的多个可能值进行判断时,switch语句通常比if...else更清晰。 let day = 3; let dayName; switch (day) { case 1: dayName = "星期一"; break; case 2: dayName = "星期二"; break; case 3: dayName = "星期三"; break; case 4: dayName = "星期四"; break; case 5: dayName = "星期五"; break; case 6: case 7: dayName = "周末"; break; default: dayName = "无效日期"; } console.log(dayName); // "星期三" **switch语句要点**: * 使用严格相等(===)进行比较 * case值可以是表达式,但会预先求值 * 多个case可以共享代码(fall-through) * default是可选的,可以放在任意位置 * 忘记break会导致case穿透 // case穿透示例(有时是有意的) let fruit = "apple"; let type; switch (fruit) { case "apple": case "pear": case "peach": type = "梨果类"; break; case "orange": case "lemon": case "grapefruit": type = "柑橘类"; break; default: type = "其他"; } ==== 3.2.4 条件表达式的替代写法 ==== // 三元运算符替代简单if...else let age = 20; let status = age >= 18 ? "成年" : "未成年"; // 逻辑与替代简单if isValid && processData(); // 等价于 if (isValid) { processData(); } // 逻辑或提供默认值 let name = inputName || "Anonymous"; // 空值合并(ES2020) let count = userCount ?? 0; // 只在null/undefined时取默认值 ===== 3.3 循环语句 ===== ==== 3.3.1 while循环 ==== 先检查条件,条件为真时执行循环体。 // 基本while循环 let count = 0; while (count < 5) { console.log(count); // 0, 1, 2, 3, 4 count++; } // 读取用户输入直到有效(示例概念) let input; while (!isValid(input)) { input = getInput(); } // 无限循环(需要break退出) while (true) { // 处理任务 if (shouldStop()) { break; } } ==== 3.3.2 do...while循环 ==== 先执行一次循环体,再检查条件。确保循环体至少执行一次。 // 基本do...while循环 let i = 0; do { console.log(i); // 0, 1, 2, 3, 4 i++; } while (i < 5); // 至少执行一次的菜单系统 let choice; do { displayMenu(); choice = getUserChoice(); processChoice(choice); } while (choice !== "quit"); ==== 3.3.3 for循环 ==== 最常用的循环结构,包含初始化、条件、迭代三个部分。 // 基本for循环 for (let i = 0; i < 5; i++) { console.log(i); // 0, 1, 2, 3, 4 } // 循环的组成部分都是可选的 let j = 0; for (; j < 5;) { console.log(j); j++; } // 多个初始化表达式 for (let i = 0, j = 10; i < j; i++, j--) { console.log(i, j); } // 递减循环 for (let i = 10; i > 0; i--) { console.log(i); // 10, 9, 8, ..., 1 } // 步长不为1 for (let i = 0; i < 20; i += 2) { console.log(i); // 0, 2, 4, 6, ..., 18 } **循环内的变量作用域**: // let声明的变量有块级作用域 for (let i = 0; i < 3; i++) { console.log(i); // 0, 1, 2 } // console.log(i); // ReferenceError: i is not defined // var声明的变量有函数作用域 for (var j = 0; j < 3; j++) { console.log(j); // 0, 1, 2 } console.log(j); // 3 // let在循环中的闭包问题 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 3, 3, 3 } for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0, 1, 2 } ==== 3.3.4 for...in循环 ==== 遍历对象的可枚举属性(包括继承的属性)。 // 遍历对象 let person = { name: "张三", age: 25, city: "北京" }; for (let key in person) { console.log(key + ": " + person[key]); } // name: 张三 // age: 25 // city: 北京 // 检查是否是自身属性(非继承) for (let key in person) { if (person.hasOwnProperty(key)) { console.log(key + ": " + person[key]); } } **for...in用于数组的问题**: // 可以遍历数组,但不推荐 let arr = ["a", "b", "c"]; for (let index in arr) { console.log(index, arr[index]); // index是字符串"0", "1", "2" } // 问题:会遍历非数字属性和原型链属性 Array.prototype.custom = "custom"; for (let i in arr) { console.log(i); // 0, 1, 2, custom } // 更好的数组遍历方式:for...of 或 forEach ==== 3.3.5 for...of循环(ES6) ==== 遍历可迭代对象(数组、字符串、Map、Set、NodeList等)。 // 遍历数组 let colors = ["red", "green", "blue"]; for (let color of colors) { console.log(color); // red, green, blue } // 遍历字符串 for (let char of "Hello") { console.log(char); // H, e, l, l, o } // 遍历Map let map = new Map([["a", 1], ["b", 2]]); for (let [key, value] of map) { console.log(key, value); } // 使用entries()获取索引和值 for (let [index, value] of colors.entries()) { console.log(index, value); } // 使用解构 let users = [ { name: "Alice", age: 25 }, { name: "Bob", age: 30 } ]; for (let { name, age } of users) { console.log(`${name} is ${age} years old`); } **for...of vs for...in**: | 特性 | for...in | for...of | |------|----------|----------| | 用途 | 遍历对象属性 | 遍历可迭代对象的值 | | 数组索引 | 字符串 | 数字 | | 遍历原型链 | 是 | 否 | | Symbol属性 | 否 | 是 | ===== 3.4 跳转语句 ===== ==== 3.4.1 break语句 ==== 立即终止当前循环或switch语句。 // 在循环中使用break for (let i = 0; i < 10; i++) { if (i === 5) { break; // 退出循环,不再执行 } console.log(i); // 0, 1, 2, 3, 4 } // 在switch中使用break(前面已有示例) // 在while中使用break let num = 1; while (true) { if (num > 100) { break; } num *= 2; } console.log(num); // 128 **标签语句与break**: // 给循环添加标签 outerLoop: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1 && j === 1) { break outerLoop; // 跳出外层循环 } console.log(i, j); } } // 输出: // 0 0 // 0 1 // 0 2 // 1 0 ==== 3.4.2 continue语句 ==== 跳过当前迭代,继续下一次循环。 // 跳过偶数 for (let i = 0; i < 10; i++) { if (i % 2 === 0) { continue; // 跳过本次循环的剩余部分 } console.log(i); // 1, 3, 5, 7, 9 } // 处理数组时跳过某些元素 let numbers = [1, 2, null, 4, undefined, 6]; let sum = 0; for (let num of numbers) { if (num === null || num === undefined) { continue; } sum += num; } console.log(sum); // 13 **continue与标签**: outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (j === 1) { continue outer; // 跳到外层循环的下一次迭代 } console.log(i, j); } } // 输出: // 0 0 // 1 0 // 2 0 ==== 3.4.3 return语句 ==== 从函数中返回一个值,并终止函数执行。 // 基本return function add(a, b) { return a + b; } // 提前返回 function divide(a, b) { if (b === 0) { return null; // 提前返回,不执行后面的代码 } return a / b; } // 无返回值(或返回undefined) function log(message) { console.log(message); return; // 可选,函数末尾自动返回undefined } // 返回多个值(通过对象或数组) function getMinMax(arr) { return { min: Math.min(...arr), max: Math.max(...arr) }; } let { min, max } = getMinMax([3, 1, 4, 1, 5]); ===== 3.5 异常处理 ===== ==== 3.5.1 try...catch...finally ==== try { // 尝试执行的代码 let result = riskyOperation(); console.log(result); } catch (error) { // 发生错误时执行 console.error("发生错误:", error.message); } finally { // 无论是否发生错误都执行 console.log("清理资源"); } **catch块参数**: // 捕获错误对象 try { JSON.parse("invalid json"); } catch (e) { console.log(e.name); // "SyntaxError" console.log(e.message); // 错误信息 console.log(e.stack); // 错误堆栈 } // ES2019可选绑定(不需要错误对象时) try { riskyOperation(); } catch { console.log("出错了,但不需要错误详情"); } ==== 3.5.2 抛出异常 ==== // 抛出Error对象 function divide(a, b) { if (b === 0) { throw new Error("除数不能为零"); } return a / b; } // 使用特定的错误类型 function validateAge(age) { if (typeof age !== 'number') { throw new TypeError("年龄必须是数字"); } if (age < 0 || age > 150) { throw new RangeError("年龄必须在0-150之间"); } } // 自定义错误类型 class ValidationError extends Error { constructor(message, field) { super(message); this.name = "ValidationError"; this.field = field; } } throw new ValidationError("用户名不能为空", "username"); ===== 3.6 循环优化与最佳实践 ===== ==== 3.6.1 循环性能优化 ==== // 1. 缓存数组长度 // 不推荐 for (let i = 0; i < array.length; i++) { } // 推荐 for (let i = 0, len = array.length; i < len; i++) { } // 或者反向循环 for (let i = array.length - 1; i >= 0; i--) { } // 2. 避免在循环中创建函数 // 不推荐 for (let i = 0; i < 10; i++) { setTimeout(() => console.log(i), 100); // 每次都创建新函数 } // 推荐:使用let块级作用域,或提取函数 for (let i = 0; i < 10; i++) { setTimeout(createLogger(i), 100); } function createLogger(i) { return () => console.log(i); } // 3. 使用合适的数据结构和方法 // 使用forEach、map、filter等替代手动循环 array.forEach(item => console.log(item)); let doubled = array.map(x => x * 2); let evens = array.filter(x => x % 2 === 0); ==== 3.6.2 循环模式 ===== **遍历数组的所有方法**: let numbers = [1, 2, 3, 4, 5]; // for循环 for (let i = 0; i < numbers.length; i++) { console.log(numbers[i]); } // for...of(推荐用于遍历值) for (let num of numbers) { console.log(num); } // forEach(不能break或return跳出) numbers.forEach((num, index) => { console.log(index, num); }); // entries()获取索引和值 for (let [index, value] of numbers.entries()) { console.log(index, value); } // some(找到满足条件的就停止) numbers.some(num => { console.log(num); return num > 3; // num > 3时停止 }); // every(找到不满足条件的就停止) numbers.every(num => { console.log(num); return num < 3; // num >= 3时停止 }); ===== 3.7 练习题 ===== ==== 练习1:条件语句 ==== 编写一个函数 `getDayType`,根据数字返回星期类型: // 1-5: 工作日 // 6-7: 周末 // 其他: 无效输入 getDayType(1); // "工作日" getDayType(6); // "周末" getDayType(0); // "无效输入" // 使用if...else和switch两种写法 ==== 练习2:循环应用 ==== 完成以下循环练习: // 1. 使用for循环计算1到100的和 // 2. 使用while循环找出100以内的所有质数 // 3. 使用do...while实现一个简单的猜数字游戏 // 生成1-100的随机数,让用户猜,提示太大或太小,直到猜中 // 4. 使用嵌套循环打印乘法表 // 1*1=1 // 1*2=2 2*2=4 // ... ==== 练习3:break和continue ==== 分析以下代码的输出: // 代码1 for (let i = 0; i < 5; i++) { if (i === 2) continue; if (i === 4) break; console.log(i); } // 代码2 outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i + j === 2) break outer; console.log(i, j); } } // 代码3 let arr = [1, 2, 3, 4, 5]; for (let i = 0; i < arr.length; i++) { if (arr[i] % 2 === 0) { arr.splice(i, 1); i--; } } console.log(arr); ==== 练习4:综合练习 ==== 编写一个函数 `fizzBuzz`,接收一个数字n,打印1到n: * 如果数字能被3整除,打印"Fizz" * 如果数字能被5整除,打印"Buzz" * 如果同时能被3和5整除,打印"FizzBuzz" * 否则打印数字本身 然后优化这个函数,使其可以接收任意数量的规则(数字和对应的字符串)。 ===== 参考答案 ===== **练习1答案**: // if...else写法 function getDayTypeIf(day) { if (day >= 1 && day <= 5) { return "工作日"; } else if (day === 6 || day === 7) { return "周末"; } else { return "无效输入"; } } // switch写法 function getDayTypeSwitch(day) { switch (day) { case 1: case 2: case 3: case 4: case 5: return "工作日"; case 6: case 7: return "周末"; default: return "无效输入"; } } **练习2答案**: // 1. 1到100的和 let sum = 0; for (let i = 1; i <= 100; i++) { sum += i; } console.log(sum); // 5050 // 2. 100以内的质数 function isPrime(n) { if (n < 2) return false; for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) return false; } return true; } let n = 2; while (n < 100) { if (isPrime(n)) { console.log(n); } n++; } // 3. 猜数字游戏(简化版) function guessNumberGame() { const target = Math.floor(Math.random() * 100) + 1; let guess; let attempts = 0; do { guess = parseInt(prompt("猜一个1-100的数字:")); attempts++; if (guess > target) { alert("太大了!"); } else if (guess < target) { alert("太小了!"); } } while (guess !== target); alert(`恭喜你猜对了!答案是${target},你用了${attempts}次。`); } // 4. 乘法表 for (let i = 1; i <= 9; i++) { let row = ""; for (let j = 1; j <= i; j++) { row += `${j}*${i}=${i*j}\t`; } console.log(row); } **练习3答案**: // 代码1:输出 0, 1, 3 // i=0: 输出0 // i=1: 输出1 // i=2: continue,跳过 // i=3: 输出3 // i=4: break,退出 // 代码2:输出 (0,0), (0,1) // i=0,j=0: 0+0=0,输出 // i=0,j=1: 0+1=1,输出 // i=0,j=2: 0+2=2,break outer // 代码3:输出 [1, 3, 5] // 删除元素时调整索引,避免跳过元素 **练习4答案**: // 基础版 function fizzBuzz(n) { for (let i = 1; i <= n; i++) { if (i % 3 === 0 && i % 5 === 0) { console.log("FizzBuzz"); } else if (i % 3 === 0) { console.log("Fizz"); } else if (i % 5 === 0) { console.log("Buzz"); } else { console.log(i); } } } // 优化版:支持任意规则 function fizzBuzzAdvanced(n, rules) { // rules: [{ divisor: 3, word: "Fizz" }, { divisor: 5, word: "Buzz" }] for (let i = 1; i <= n; i++) { let output = ""; for (let rule of rules) { if (i % rule.divisor === 0) { output += rule.word; } } console.log(output || i); } } // 使用 fizzBuzzAdvanced(30, [ { divisor: 3, word: "Fizz" }, { divisor: 5, word: "Buzz" }, { divisor: 7, word: "Whizz" } ]); ===== 本章小结 ===== 本章学习了JavaScript的控制流语句: * **条件语句**:if...else、switch、三元运算符 * **循环语句**:while、do...while、for、for...in、for...of * **跳转语句**:break、continue、return * **异常处理**:try...catch...finally、throw 关键要点: * 根据场景选择合适的循环结构 * for...of适合遍历可迭代对象的值 * for...in适合遍历对象属性(数组不推荐) * 合理使用break和continue控制循环流程 * 掌握异常处理机制编写健壮的代码 下一章将学习函数,这是JavaScript中最重要的概念之一。