====== 第三章 控制流 ======
===== 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中最重要的概念之一。