JavaScript学习笔记:4.循环与迭代
上一篇咱们搞定了JS的"决策术"和"容错术"(控制流与错误处理),这一篇来解锁JS的"高效干活技能"------循环与迭代。如果说条件语句是让JS"会做选择",那循环就是让JS"会重复做事":比如批量处理数据、遍历数组、循环请求接口......本质上都是"重复执行一段代码"。
但JS的循环家族成员不少(for、while、do...while、for...in、for...of),各自有擅长的场景,也藏着不少"坑"。今天就用"生活化比喻+实战避坑"的方式,带你吃透这些循环,从此重复工作"一键搞定",不做无用功~
一、循环三剑客:for、while、do...while------基础重复操作指南
循环三剑客是JS最基础的循环语句,核心作用都是"重复执行代码",但适用场景和执行逻辑天差地别,就像三种不同的"干活模式"。
1. for循环:有明确步骤的"精准干活"
for循环就像"按流程做事的强迫症",有明确的"初始化-条件判断-更新步骤",适合知道循环次数或有明确边界的场景。语法结构:
js
for (初始化变量; 循环条件; 更新变量) {
重复做的事;
}
比如"打包5个快递",步骤清晰:初始化(开始打包第1个)、条件(没到5个就继续)、更新(打包下一个):
js
// 打包5个快递
for (let i = 1; i <= 5; i++) {
console.log(`打包第${i}个快递`);
}
// 输出:打包第1个快递 → 打包第2个 → ... → 打包第5个
核心坑:var vs let的"变量污染陷阱"
这是新手最容易栽的坑!用var声明循环变量会出现"变量提升+函数作用域"的问题,导致循环结束后变量值"串味":
js
// 反面例子:用var声明循环变量
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:4、4、4(而不是1、2、3)
原因:var声明的i是函数/全局作用域,循环结束后i变成4,setTimeout异步执行时拿到的都是最终的i。
避坑指南:循环变量必须用let声明!let是块级作用域,每次循环都会创建一个独立的i,不会串味:
js
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:1、2、3(正确)
进阶技巧:省略部分表达式
for循环的三个表达式都可以省略,但分号不能少:
- 省略初始化:
for (; i < 5; i++)(变量在外部声明) - 省略条件:
for (let i = 1; ; i++)(变成无限循环,需在内部用break终止) - 省略更新:
for (let i = 1; i < 5; ) { i++; }(更新逻辑写在循环体内)
2. while循环:条件满足就"一直干"
while循环就像"只要条件允许就不停干活",适合不知道循环次数,但知道"停止条件"的场景。语法:
js
while (循环条件) {
重复做的事;
}
比如"只要奶茶没喝完,就一直吸":
js
let 奶茶剩余量 = 50; // 单位:ml
while (奶茶剩余量 > 0) {
奶茶剩余量 -= 10;
console.log(`吸了10ml,还剩${奶茶剩余量}ml`);
}
// 输出:吸了10ml,还剩40ml → ... → 吸了10ml,还剩0ml
致命坑:无限循环!
while循环的条件如果永远为true,就会陷入无限循环,直接让浏览器卡死(比如忘记更新循环变量):
js
// 反面例子:无限循环(永远true)
while (true) {
console.log("一直输出,停不下来!");
}
避坑指南:确保循环体内有"让条件变false"的逻辑(比如更新变量、break语句),永远不要写无终止条件的while(true)(除非故意用break控制)。
3. do...while循环:先干一次,再看条件
do...while循环是"冲动型干活":不管条件满足与否,先执行一次循环体,再判断是否继续。语法:
js
do {
重复做的事;
} while (循环条件);
比如"先喝一口奶茶,再看要不要续杯":
js
let 想续杯 = false;
do {
console.log("先喝一口奶茶");
} while (想续杯);
// 输出:先喝一口奶茶(即使想续杯是false,也执行了一次)
适用场景:必须执行一次的操作
比如用户登录时"先验证一次表单,再判断是否重新输入"、初始化数据时"先加载一次,再判断是否需要更新"。
避坑点:分号不能漏!
do...while的结尾必须加分号(while (条件);),否则会报错------这是唯一需要结尾加分号的循环语句。
三剑客对比表:该选谁?
| 循环类型 | 执行逻辑 | 适用场景 | 核心注意点 |
|---|---|---|---|
| for | 初始化→条件→执行→更新 | 知道循环次数/有明确边界 | 用let声明变量,避免污染 |
| while | 条件→执行→更新 | 不知道次数,但知道停止条件 | 防止无限循环 |
| do...while | 执行→条件→更新 | 必须执行至少一次 | 结尾加分号 |
二、循环控制符:break与continue------循环的"刹车"与"跳过"
如果说循环是"自动跑步机",那break和continue就是"刹车"和"跳过当前坡度"------用来控制循环的执行流程,避免无效执行。
1. break:直接"停掉跑步机"
break的作用是"立即终止当前循环/switch",不管后续条件是否满足。比如"找数组里的目标值,找到就停":
js
const 水果数组 = ["苹果", "香蕉", "橙子", "葡萄"];
let 目标水果 = "橙子";
for (let i = 0; i < 水果数组.length; i++) {
if (水果数组[i] === 目标水果) {
console.log(`找到${目标水果},索引是${i}`);
break; // 找到就终止循环,不用再找了
}
}
// 输出:找到橙子,索引是2(循环只执行3次,不是4次)
2. continue:"跳过当前步,继续下一轮"
continue的作用是"跳过循环体剩余代码,直接进入下一轮循环",不会终止整个循环。比如"筛选数组,只打印偶数":
js
for (let i = 1; i <= 5; i++) {
if (i % 2 !== 0) {
continue; // 不是偶数,跳过后面的打印
}
console.log(`偶数:${i}`);
}
// 输出:偶数:2 → 偶数:4
3. 易错点:break vs continue的区别
很多新手会搞混两者:
- break:"我不干了,整个循环都停"
- continue:"这一轮不干了,下一轮再来"
举个例子,同样是"遇到3就操作":
js
// break版本:遇到3就停
for (let i = 1; i <= 5; i++) {
if (i === 3) break;
console.log(i); // 输出1、2
}
// continue版本:遇到3跳过,继续下一轮
for (let i = 1; i <= 5; i++) {
if (i === 3) continue;
console.log(i); // 输出1、2、4、5
}
三、label语句:多层循环的"精准导航"
当遇到"循环嵌套"(比如双层for循环)时,break和continue默认只作用于"当前循环",这时候label语句就能派上用场------给循环贴个"标签",让break/continue精准控制外层循环。
label就像"给循环起个名字",语法:
js
标签名: 循环语句 {
// 循环体
}
比如"双层循环找坐标(5,5),找到就终止所有循环":
js
// 反面例子:没有label,break只终止内层循环
let 计数 = 0;
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i === 5 && j === 5) break; // 只终止内层j循环,i循环继续
计数++;
}
}
console.log(计数); // 输出95(内层循环到5就停,但i还会继续到9)
// 正面例子:用label,break终止外层循环
let 计数2 = 0;
外层循环: for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i === 5 && j === 5) break 外层循环; // 直接终止外层循环
计数2++;
}
}
console.log(计数2); // 输出55(找到(5,5)就停,总共执行55次)
避坑指南:
- label只能标识"循环语句"或"块语句",不能标识单独的语句。
- 不要滥用label:多层循环很少见,用label会让代码可读性变差,能拆分成函数就尽量拆分。
四、迭代神器:for...in与for...of------遍历对象/数组的"专属工具"
如果说基础循环是"通用工具",那for...in和for...of就是"专用工具"------专门用来遍历对象或可迭代对象(数组、Map、Set等),比基础循环更简洁。
1. for...in:遍历对象的"属性探测器"
for...in的作用是"遍历对象的所有可枚举属性",包括原型链上的属性。语法:
js
for (let 属性名 in 对象) {
操作属性;
}
比如遍历汽车对象的属性:
js
const 汽车 = { 品牌: "特斯拉", 型号: "Model 3", 价格: 23.99 };
for (let key in 汽车) {
console.log(`${key}:${汽车[key]}`);
}
// 输出:品牌:特斯拉 → 型号:Model 3 → 价格:23.99
致命坑:千万别用for...in遍历数组!
很多新手会犯这个错,但for...in遍历数组有两个致命问题:
- 遍历的是"索引+自定义属性":数组的自定义属性也会被遍历到,而不是只遍历元素。
- 遍历顺序不固定:可能不是按数组索引顺序遍历。
js
// 反面例子:for...in遍历数组
const 水果 = ["苹果", "香蕉", "橙子"];
水果.产地 = "中国"; // 给数组加个自定义属性
for (let i in 水果) {
console.log(i); // 输出0、1、2、产地(把自定义属性也遍历了!)
}
正确用法:
-
只用来遍历"普通对象"的属性。
-
遍历对象时,用
hasOwnProperty过滤原型链上的属性(避免遍历到继承的属性):jsfor (let key in 汽车) { if (汽车.hasOwnProperty(key)) { // 只遍历自身属性 console.log(`${key}:${汽车[key]}`); } }
2. for...of:遍历可迭代对象的"值提取器"
for...of是ES6新增的迭代语句,专门用来遍历"可迭代对象"(数组、Map、Set、字符串、arguments等),直接遍历"值"而不是索引或属性,比for...in更安全、更简洁。语法:
js
for (let 值 of 可迭代对象) {
操作值;
}
核心优势:
-
遍历数组:直接拿元素值,不关心索引,也不会遍历自定义属性:
jsconst 水果 = ["苹果", "香蕉", "橙子"];
水果.产地 = "中国";
for (let 果 of 水果) {
console.log(果); // 输出苹果、香蕉、橙子(忽略自定义属性)
}
- 遍历字符串:直接拿每个字符:
```js
for (let 字符 of "前端开发") {
console.log(字符); // 输出前、端、开、发
}
-
遍历Map/Set:直接拿键值对或元素,比for循环简洁太多:
jsconst 学生成绩 = new Map([["小明", 90], ["小红", 85]]); for (let [姓名, 分数] of 学生成绩) { console.log(`${姓名}:${分数}`); // 输出小明:90 → 小红:85 }
避坑指南:
-
不能直接遍历"普通对象":普通对象不是可迭代对象,用for...of遍历会报错。如果要遍历对象,先转成可迭代对象(比如
Object.values(对象)):jsconst 汽车 = { 品牌: "特斯拉", 型号: "Model 3" }; // 正确:先转成值数组 for (let 值 of Object.values(汽车)) { console.log(值); // 输出特斯拉、Model 3 } -
可以用
break/continue控制循环:和基础循环一样支持循环控制符。
for...in vs for...of 对比表
| 特性 | for...in | for...of |
|---|---|---|
| 遍历目标 | 对象的可枚举属性(含原型链) | 可迭代对象的值(数组、Map等) |
| 数组遍历 | 遍历索引+自定义属性(不推荐) | 遍历元素值(推荐) |
| 对象遍历 | 直接遍历(需过滤原型链) | 需转成可迭代对象(如Object.values) |
| 支持的控制符 | break/continue | break/continue |
五、循环实战避坑总结
- 基础循环选对场景:知道次数用for,不知道次数用while,必须执行一次用do...while。
- 循环变量用let:避免var的变量污染和异步问题。
- 防止无限循环:while循环必须有终止条件,for循环不能省略更新表达式。
- 遍历数组用for...of:别用for...in,否则会遍历自定义属性。
- 遍历对象用for...in+hasOwnProperty:或Object.values+for...of。
- 多层循环少用label:尽量拆分成函数,提高可读性。
- break和continue别搞混:break终止循环,continue跳过当前轮。
六、最后:循环的"效率秘籍"
-
减少循环内的计算:把循环外能算的东西(比如数组长度)提前缓存,避免每次循环都计算:
js// 优化前:每次循环都计算arr.length for (let i = 0; i < arr.length; i++) {} // 优化后:缓存长度 const len = arr.length; for (let i = 0; i < len; i++) {} -
避免循环嵌套:嵌套循环的时间复杂度是O(n²),数据量大时会卡顿,能扁平化数据就扁平化。
-
优先用数组方法:forEach、map、filter等方法(本质也是循环),比手动写for循环更简洁,但要注意forEach不能用break/continue终止(除非抛异常)。
循环与迭代是JS处理重复任务的核心,选对循环类型、避开常见坑,能让你的代码既简洁又高效。下一篇笔记,我们会聊JS的函数------这是JS的"代码复用神器",让你写出可复用、高内聚的代码。关注我,继续解锁JS的实战技能,从"会写"到"写好",一步步成为JS高手~