做编程教学这几年我接触过不少从零开始学 JavaScript 的朋友。很多人学到变量、函数的时候都挺顺利但一到循环结构就开始卡壳写出来的代码要么进不了循环要么直接死循环把浏览器卡死。说白了循环不是“背语法”那么简单的玩意儿它考验的是你对“重复执行的边界”有没有想清楚。这篇文章就把循环结构从底层逻辑到实操一次性讲透再配上 15 道我自己整理、带参考答案的练习题跟着走一遍你对循环的理解会比啃十篇文档都管用。1. 为什么循环是JavaScript学习的分水岭1.1 循环解决的到底是什么问题先想一个场景你要打印 1 到 100 的数字。不用循环的话你得写 100 行console.log(1)、console.log(2)……这不现实。循环的本质就是“让同一段代码在条件控制下反复执行”把重复劳动交给机器。它的核心价值不是省几行代码而是让程序具备处理批量数据的能力。我在教学中发现很多人对循环的理解停在了“语法层面”——知道for怎么写、while怎么写但一遇到实际问题就懵。为什么因为循环真正的难点在于三个问题的思考从哪里开始初始值、什么时候结束条件、每一步怎么变化步长。想清楚这三个问题任何循环题都难不倒你。1.2 从“手工重复”到“自动化执行”的思维转变学循环前你先要完成一个思维升级把“我要做什么”变成“我要重复做什么”。举个生活化的例子。你去超市买 10 瓶水人的做法是“拿一瓶、再拿一瓶、再拿一瓶……直到拿够 10 瓶”。这个过程的循环化表达就是let count 0; while (count 10) { count; console.log(拿第 count 瓶水); }初始值count 0条件count 10步长是count。你看生活里你觉得理所当然的事写代码时就得把每个细节都摆到台面上。这种“把直觉变成严谨逻辑”的能力正是循环学习的真正意义也是后面学算法、学数据结构的地基。2. 六种循环结构逐一拆解2.1 for循环最精确的计数循环for循环是三段式结构一次把初始化、条件、更新都写清楚特别适合“知道要循环多少次”的场景。for (let i 0; i 5; i) { console.log(第 i 次执行); }这里有个初学者最容易忽略的细节let i 0是在进入循环前只执行一次的初始化i是在每次循环体执行完毕后才执行。整个流程是初始化 → 判断条件 → 执行体 → 更新 → 判断条件 → 执行体 → ……直到条件为false跳出。我见过太多人问“为什么我的 i 是从 0 开始不是从 1 开始”因为数组的下标就是从 0 开始的这是 JavaScript以及几乎所有主流语言的通用约定。你写for (let i 0; i arr.length; i)就能恰好遍历整个数组一个不多一个不少。这个约定背后是内存寻址的偏移量逻辑——首元素偏移 0第二个偏移 1以此类推。理解这一点你就不会纠结 0 和 1 的问题了。2.2 while与do-while条件驱动的循环while适合“不知道具体次数只知道什么时候停止”的场景。比如你不断读取用户输入直到输入“quit”才停止let input ; while (input ! quit) { input prompt(请输入内容输入 quit 结束); }注意while是先判断后执行条件一开始不满足循环体一次都不会执行。do-while则刚好相反先执行一次再判断条件。这在需要“至少执行一次”的场景里特别有用比如菜单展示let choice; do { choice prompt(请选择操作1-查询2-退出); } while (choice ! 2);两者的差别就一句话do-while保证至少跑一次。做练习题的时候每次见到“直到……”和“至少……”这样的字眼就要考虑是不是该用do-while。2.3 for...of与for...in遍历数据的两种思路很多初学者分不清for...of和for...in我用一句话给你点透for...of遍历的是“值”for...in遍历的是“键”。const arr [a, b, c]; for (const item of arr) { console.log(item); // 输出a、b、c } for (const index in arr) { console.log(index); // 输出0、1、2 }实践中95% 的情况你该用for...of因为它直接给你值代码可读性更好。for...in更适合遍历对象属性但使用时要加上hasOwnProperty判断避免遍历到原型链上的属性const user { name: 张三, age: 25 }; for (const key in user) { if (Object.hasOwn(user, key)) { console.log(key user[key]); } }2.4 forEach与map函数式循环的优雅之处数组的forEach方法其实也是一种循环只是用了回调函数的形式。它比传统for更简洁也更安全因为不需要手动管理索引const numbers [1, 2, 3, 4, 5]; numbers.forEach((num, index) { console.log(下标 ${index} 对应的值是 ${num}); });不过这里有个常被忽略的坑forEach里用break或continue是无效的会直接报语法错误。如果你需要在循环中途跳出老老实实用for或者for...of。forEach适合“每个元素都要处理、不需要中断”的场景而map则是“我要把数组映射成新数组”时用const doubled numbers.map(num num * 2); // doubled [2, 4, 6, 8, 10]我把这些循环方式整理成了一张对比表做题之前先对照看循环方式适用场景能否打断能否拿索引推荐度for明确次数的计数循环能能五星while条件未知的循环能视写法四星do-while至少执行一次的场景能视写法三星for...of遍历可迭代对象的值能配合entries五星for...in遍历对象键名能本身就是键二星forEach数组全量处理不能能四星3. 实操过程用循环解决经典问题3.1 累加求和从数学公式到循环落地累加是循环最经典的应用。比如求 1 到 100 的和。你可能知道高斯公式(首项末项)×项数/2但循环的写法学的是“怎么让计算机一步步累加”let sum 0; for (let i 1; i 100; i) { sum i; } console.log(sum); // 5050注意sum必须定义在循环外面否则每次循环都被重置为 0永远算不对。这个错误我在学生代码里见得太多了。再进阶一点求数组元素的平均值const scores [85, 92, 78, 96, 88]; let total 0; for (const score of scores) { total score; } const avg total / scores.length; console.log(avg); // 87.83.2 遍历数组与对象数据处理的基本功实际开发中循环干得最多的就是处理数组数据。比如从商品列表里找出价格大于 100 的商品const products [ { name: 鼠标, price: 79 }, { name: 键盘, price: 199 }, { name: 显示器, price: 1299 }, { name: U盘, price: 59 } ]; const expensive []; for (const product of products) { if (product.price 100) { expensive.push(product); } } console.log(expensive);这里我用for...of直接取出数组里的每个对象再访问对象的price属性。你看数组和对象的知识点就这么自然地结合起来了。处理对象时配合Object.entries可以同时拿到键和值const userInfo { name: 李四, city: 杭州, job: 前端开发 }; for (const [key, value] of Object.entries(userInfo)) { console.log(${key}${value}); }3.3 循环控制流break、continue、标签break是“提前终止整个循环”continue是“跳过本次剩余代码进入下一次循环”。看这个例子输出 1 到 10 中所有不是 3 的倍数的数for (let i 1; i 10; i) { if (i % 3 0) { continue; } console.log(i); // 1、2、4、5、7、8、10 }continue不会退出循环只是跳过当前这一轮。而break会直接跳出整个循环结构let sum 0; for (let i 1; i 100; i) { if (sum 50) break; sum i; } console.log(sum); // 55加到10时就超过50了标签label是多层循环里跳出内层、直接控制外层用的。嵌套循环里想“跳出所有层”时标签就派上用场了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${i}, j${j}); } } // 输出到 i1, j1 之前就停了标签语法在实战里用得不多但训练逻辑的时候它是很好的思维体操。4. 常见问题与排查技巧实录4.1 死循环页面为什么卡死了死循环大概是循环学习中最容易遇到的灾难题。典型的写法是把条件写成了永远成立// 错误示例 for (let i 0; i 10; i--) { console.log(i); }i--让数值越来越小i 10永远成立浏览器直接卡死。排查死循环第一件事就是看变量是往条件相反的方向走还是往满足条件的方向走。如果变量一直在让条件保持为真那就是死循环。4.2 边界条件差一错误循环里最经典的 bug 是“差一”。i arr.length还是i arr.length少一个等号结果天差地别。const arr [10, 20, 30]; for (let i 0; i arr.length; i) { console.log(arr[i]); // 最后一次输出 undefined }i arr.length会多循环一次访问到不存在的arr[3]输出undefined。我的口诀是数组长度为 n有效下标 0 到 n-1循环条件一定是i n少用。4.3 循环里的闭包陷阱这个坑在面试里出现率极高。你用var在循环里声明变量再配合异步回调就会发现回调里的变量全变成了最后一个值for (var i 0; i 5; i) { setTimeout(() { console.log(i); }, 100); } // 输出5、5、5、5、5为什么因为var是函数作用域循环没有形成独立作用域所有回调共享同一个i等回调执行时i已经变成 5 了。解决办法有两个把var换成let块级作用域每次循环都是新的绑定或者用闭包把值临时存起来。这个例子强烈建议自己跑一遍理解了它你就真正理解作用域了。4.4 性能优化别在循环里做重复计算循环性能的常见问题是“每次循环都重复计算一个不变的值”。比如// 性能较差每次循环都重新取 length for (let i 0; i arr.length; i) { // 处理逻辑 } // 性能更好length 先存起来 for (let i 0, len arr.length; i len; i) { // 处理逻辑 }虽然现代 JS 引擎已经做了很多优化两者差距没有想象中大但写代码时养成“把不变量提出去”的习惯对算法思维是有利的。另一个优化点是能用for的地方不用forEach能用for...of的地方不用for...in因为for...in要遍历属性链开销更大。5. 15道循环练习题详解从易到难5.1 基础题1-5先把语法写熟练题目1打印 1 到 100 之间的所有整数。for (let i 1; i 100; i) { console.log(i); }题目2计算 1 到 20 的累加和。let sum 0; for (let i 1; i 20; i) { sum i; } console.log(sum); // 210题目3打印 1 到 50 之间的所有偶数。for (let i 1; i 50; i) { if (i % 2 0) { console.log(i); } }这里有个小优化可以直接让 i 以 2 为步长从 2 开始避免每一次都取模for (let i 2; i 50; i 2) { console.log(i); }两种写法都对但后者更能体现你对循环步长的理解。题目4计算 1 到 10 的乘积即 10!。let result 1; for (let i 1; i 10; i) { result * i; } console.log(result); // 3628800注意初始值必须是 1不是 0——任何数乘 0 都变成 0 了。这是初学者最容易犯的错。题目5反向打印数组[a, b, c, d]。const arr [a, b, c, d]; for (let i arr.length - 1; i 0; i--) { console.log(arr[i]); // d、c、b、a }反向遍历的关键是初始值设为arr.length - 1最后一个元素的下标条件设为i 0。5.2 进阶题6-10数据结构和逻辑组合题目6求数组[12, 5, 8, 99, 34]的最大值和最小值。const nums [12, 5, 8, 99, 34]; let max nums[0]; let min nums[0]; for (let i 1; i nums.length; i) { if (nums[i] max) max nums[i]; if (nums[i] min) min nums[i]; } console.log(最大值${max}最小值${min});要理解为什么把最大值初始值设成nums[0]而不是 0如果数组元素全是负数用 0 当初始值永远找不到真正的最大值。用数组第一个元素做基准是最安全可靠的。题目7数组去重把[1, 2, 2, 3, 4, 4, 4, 5]去重后输出。const raw [1, 2, 2, 3, 4, 4, 4, 5]; const unique []; for (const item of raw) { if (!unique.includes(item)) { unique.push(item); } } console.log(unique); // [1, 2, 3, 4, 5]更简洁的解法是利用Set但练习题的意义在于你能不能用循环自己想出“判断是否已存在”的逻辑const unique2 [...new Set(raw)];两种方法都该掌握先用循环理解过程再用 API 简化代码。题目8打印九九乘法表。for (let i 1; i 9; i) { let rowText ; for (let j 1; j i; j) { rowText ${j} × ${i} ${i * j} ; } console.log(rowText); }这是嵌套循环的经典入门案例内层循环的次数依赖外层循环的当前值。理解j i这个条件就是理解嵌套循环的关键——它让输出成了三角形。题目9输出斐波那契数列的前 15 项。let a 1; let b 1; console.log(a); console.log(b); for (let i 3; i 15; i) { let c a b; console.log(c); a b; b c; }斐波那契的规律是“每一项等于前两项之和”。这里用变量交换的方式滚动更新先算出新值再把旧值往前推进。类似“三个杯子倒水”的思路在逻辑思维训练里非常经典。题目10反转字符串hello。const str hello; let reversed ; for (let i str.length - 1; i 0; i--) { reversed str[i]; } console.log(reversed); // olleh字符串可以像数组一样通过下标访问字符str[i]然后从后往前拼接即可。这个题考的就是对字符串特性和循环方向的综合掌握。5.3 挑战题11-15算法思维与综合应用题目11判断一个数是不是素数。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; } console.log(isPrime(17)); // true console.log(isPrime(18)); // false这个题的精髓在i Math.sqrt(n)。为什么只跑到平方根就够了因为若 n 有一个大于平方根的因子它必然有一个小于平方根的对应因子。用数学性质大幅减少循环次数是算法优化的启蒙思想。题目12统计字符串hello world中每个字符出现的次数。const str hello world; const countMap {}; for (const char of str) { if (countMap[char]) { countMap[char]; } else { countMap[char] 1; } } console.log(countMap); // { h: 1, e: 1, l: 3, o: 2, : 1, w: 1, r: 1, d: 1 }这个题用对象当容器把字符作为键、次数作为值。对字符串for...of会逐字符遍历空格也是一个字符。这是后面做词频统计、文本分析的基础。题目13用冒泡排序把数组[64, 34, 25, 12, 22, 11, 90]升序排列。const arr [64, 34, 25, 12, 22, 11, 90]; for (let i 0; i arr.length - 1; i) { for (let j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { [arr[j], arr[j 1]] [arr[j 1], arr[j]]; } } } console.log(arr); // [11, 12, 22, 25, 34, 64, 90]冒泡排序是嵌套循环最经典的实战。内层循环每跑一轮就会把最大的数“冒泡”到最后所以内层的比较范围可以逐步缩小arr.length - 1 - i的- i就是这个含义。虽然实际开发里直接用sort但自己实现一遍能让你对算法思维的理解上一个台阶。题目14找出数组[apple, banana, cherry, date]中最长的字符串。const words [apple, banana, cherry, date]; let longest ; for (const word of words) { if (word.length longest.length) { longest word; } } console.log(longest); // banana思路和求最大值一模一样用一个变量保存当前最优解循环中不断比较更新。这种“打擂台”思维在编程里无处不在——从找最大值到求最长路径都适用。题目15打印 1 到 100 中满足“既是 3 的倍数又是 5 的倍数”的数同时统计个数。let count 0; for (let i 1; i 100; i) { if (i % 3 0 i % 5 0) { console.log(i); count; } } console.log(共 ${count} 个);这个题考的是逻辑运算和计数器的配合。注意“同时是 3 和 5 的倍数”可以直接用i % 15 0判断但写成两个条件更直观、更易读。计数器count必须放在if里每找到一个符合条件的数就加 1。最后说几句实在的循环这套东西光看永远学不会。我自己教过这么多学生总结下来就一句先把 15 道题自己做一遍别急着看答案卡住了再回头看对应章节的知识点。等你把每道题都能不假思索地写出答案再往后学数组方法、对象操作、算法基础都会顺畅很多。最后送你一个排查小技巧遇到循环结果不对不要盯着代码干看在循环体第一行加一句console.log打印关键变量看它每一轮是怎么变化的问题基本一眼就能暴露。这是最快、最土的调试方式但也是最好用的。