一、循环的本质:JavaScript 的迭代哲学
循环是编程中处理重复逻辑的核心范式,而 JavaScript 的循环体系兼具灵活性与特殊性 ------ 它不仅继承了传统结构化编程的循环思想,更融入了函数式编程的迭代理念。在 JS 引擎中,循环的本质是通过条件判断控制代码块的重复执行,但不同循环的底层执行机制、适用场景和性能表现存在显著差异。
关键认知:JS 循环的性能瓶颈往往不在于循环本身,而在于循环体内部的操作(如 DOM 操作、冗余计算)和迭代器的效率。
二、JavaScript 循环家族全解析(附场景对比)
1. 基础循环:for /while/do...while
这三类是最经典的循环形式,基于 "初始化 - 条件判断 - 迭代" 的三段式逻辑,适用于大多数简单迭代场景。
for 循环:结构清晰,适合已知循环次数的场景
// 高效遍历数组(缓存长度,避免重复计算)
const arr = [1, 2, 3, 4, 5];
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i]); // 直接访问索引,性能最优
}
while 循环:适合未知循环次数,依赖动态条件终止
// 处理异步队列(模拟消息消费)
const queue = [/* 任务列表 */];
while (queue.length > 0) {
const task = queue.shift();
executeTask(task); // 任务执行完才继续循环
}
do...while 循环:至少执行一次,适合 "先执行后判断" 场景
// 表单验证(确保用户至少输入一次)
let input;
do {
input = prompt("请输入用户名(不能为空)");
} while (!input?.trim());
2. 数组专用循环:forEach /map/filter /reduce
ES5 引入的数组迭代方法,基于函数式编程思想,代码更简洁,避免了索引操作的冗余,但需注意其特性差异:
|---------|------------|---------|---------------|----------|
| 方法 | 核心作用 | 是否改变原数组 | 返回值 | 中断支持 |
| forEach | 遍历数组执行回调 | 否(回调决定) | undefined | 不支持(需抛错) |
| map | 遍历数组返回新数组 | 否 | 新数组(长度与原数组一致) | 不支持 |
| filter | 筛选数组元素 | 否 | 符合条件的新数组 | 不支持 |
| reduce | 累加计算(聚合数组) | 否 | 最终累加结果 | 不支持 |
实战案例:数据处理流水线
const users = [
{ id: 1, name: "张三", age: 25, isVip: true },
{ id: 2, name: "李四", age: 17, isVip: false },
{ id: 3, name: "王五", age: 32, isVip: true }
];
// 筛选成年VIP -> 提取姓名 -> 拼接欢迎语
const greetings = users
.filter(user => user.age >= 18 && user.isVip) // 筛选:[张三, 王五]
.map(user => user.name) // 提取:["张三", "王五"]
.reduce((acc, name) => {
acc.push(`欢迎VIP用户:${name}`);
return acc;
}, []); // 聚合:["欢迎VIP用户:张三", "欢迎VIP用户:王五"]
3. 高阶循环:for...in/for...of/ 迭代器
ES6 + 引入的现代化循环方式,解决了传统循环的痛点,支持更多数据类型:
for...in:遍历对象可枚举属性(含原型链)
const obj = { a: 1, b: 2 };
// 安全遍历(排除原型链属性)
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(`${key}: ${obj[key]}`);
}
}
⚠️ 避坑:不可用于数组遍历!会遍历数组的非数字属性(如arr.name),且索引为字符串类型。
for...of:遍历可迭代对象(数组、字符串、Map、Set 等)
// 遍历数组(支持break/continue)
const fruits = ["苹果", "香蕉", "橙子"];
for (const fruit of fruits) {
if (fruit === "香蕉") break;
console.log(fruit); // 输出:苹果
}
// 遍历Map(键值对)
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// 遍历字符串(Unicode字符)
for (const char of "𠮷abc") {
console.log(char); // 正确输出:𠮷、a、b、c(解决for循环UTF-16代理对问题)
}
迭代器(Iterator):自定义遍历逻辑
// 生成1-5的迭代器
const createIterator = (start, end) => {
let current = start;
return {
[Symbol.iterator]() { return this; },
next() {
return current ? { value: current++, done: false }
: { done: true };
}
};
};
// 遍历自定义迭代器
for (const num of createIterator(1, 5)) {
console.log(num); // 1,2,3,4,5
}
三、95 分必备:循环性能优化实战
要达到高分水准,必须掌握 "场景匹配 + 性能优化" 的核心逻辑,以下是关键技巧:
1. 循环类型选择原则
- 数组遍历优先用for(已知长度)或for...of(需中断 / 遍历复杂类型)
- 数据转换 / 筛选用map/filter/reduce(代码简洁,可读性高)
- 对象遍历用for...in(需过滤原型链)或Object.keys()+for
- 大数据量遍历避免forEach(无法中断,性能略逊)
2. 性能优化技巧
- 缓存长度:数组遍历前缓存length,避免每次循环重新计算(尤其对于 DOM 集合)
// 低效:const lis = document.getElementsByTagName("li");(实时集合,每次访问length都重新查询DOM)
const lis = Array.from(document.getElementsByTagName("li"));
for (let i = 0, len = lis.length; i ; i++) { // 缓存len
// 操作lis[i]
}
- 减少循环体操作:将循环体内的常量、函数调用移到外部
// 低效
for (let i = 0; i 00; i++) {
console.log(Math.random() * 10); // Math.random()每次循环都调用
}
// 高效
const random = Math.random;
for (let i = 0; i < 1000; i++) {
console.log(random() * 10); // 缓存函数引用
}
- 避免不必要的 DOM 操作:循环中 DOM 操作会导致浏览器频繁重排重绘,应批量处理
// 低效:每次循环都修改DOM
const list = document.getElementById("list");
for (let i = 0; i 00; i++) {
list.innerHTML += `<li>${i};
}
// 高效:先拼接字符串,再一次性插入
let html = "";
for (let i = 0; i < 100; i++) {
html += `<li>${i}>`;
}
list.innerHTML = html;
3. 循环中断的正确方式
|------------|------------------------|------------------|
| 循环类型 | 中断方式 | 注意事项 |
| for/while | break(终止)/continue(跳过) | 直接使用,无副作用 |
| for...of | break/continue | 支持,且不影响迭代器状态 |
| forEach | 抛出异常(不推荐) | 会终止循环,但代码不优雅 |
| map/filter | 无法中断 | 需改用 for/for...of |
四、高频面试题:循环进阶考点
- forEach 与 for 循环的性能差异:
-
- for 循环直接访问索引,无函数调用开销,性能更优;
-
- forEach 需执行回调函数,且无法中断,大数据量下性能略逊。
- for...in 遍历数组的问题:
-
- 索引为字符串类型,可能导致i + 1等运算错误;
-
- 会遍历数组的非数字属性(如arr.prototype.method);
-
- 遍历顺序不固定(依赖浏览器实现)。
- 异步循环的陷阱(经典面试题):
// 期望:1秒后输出0-4,实际输出5个5
for (var i = 0; i 5; i++) {
setTimeout(() => console.log(i), 1000);
}
// 解决方案1:使用let(块级作用域)
for (let i = 0; i ++) {
setTimeout(() => console.log(i), 1000);
}
// 解决方案2:闭包保存变量
for (var i = 0; i {
(function(j) {
setTimeout(() => console.log(j), 1000);
})(i);
}