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