数组 forEach

1. 基本原理和实现

原生实现原理

javascript 复制代码
// 模拟实现 forEach
Array.prototype.myForEach = function(callback, thisArg) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  
  // 转换为对象
  const O = Object(this);
  // 转换为数字(length)
  const len = O.length >>> 0;  // 无符号右移确保是正整数
  
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  let k = 0;
  while (k < len) {
    // 检查索引是否存在(处理稀疏数组)
    if (k in O) {
      // 执行回调,传递三个参数
      callback.call(thisArg, O[k], k, O);
    }
    k++;
  }
};

// 使用示例
[1, 2, 3].myForEach((item, index) => {
  console.log(item, index);
});

2. 详细参数解析

c 复制代码
arr.forEach(callback(currentValue, index, array), thisArg)

参数详解:

javascript 复制代码
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

// 1. 完整参数使用
users.forEach(function(user, index, array) {
  console.log(`用户${index + 1}:`, user.name);
  console.log('总人数:', array.length);
  console.log('this:', this);  // thisArg 参数
}, { prefix: '用户信息:' });

// 2. 箭头函数 vs 普通函数
const obj = {
  data: [1, 2, 3],
  multiplier: 2,
  
  processData() {
    // 箭头函数 - 继承外层 this
    this.data.forEach(item => {
      console.log(item * this.multiplier);  // ✅ 正确访问 this.multiplier
    });
    
    // 普通函数 - 需要 bind 或 thisArg
    this.data.forEach(function(item) {
      console.log(item * this.multiplier);  // ❌ this 指向 undefined 或全局
    }, this);  // ✅ 传递 thisArg
    
    this.data.forEach(function(item) {
      console.log(item * this.multiplier);  
    }.bind(this));  // ✅ 使用 bind
  }
};

3. 核心特点详解

特点 1:遍历顺序固定

javascript 复制代码
const arr = ['a', 'b', 'c'];

// 总是按索引顺序执行
arr.forEach((item, index) => {
  console.log(index, item);  // 0 'a', 1 'b', 2 'c'
});

// 即使有异步操作,顺序也不会改变
arr.forEach(async (item, index) => {
  await new Promise(resolve => setTimeout(resolve, Math.random() * 100));
  console.log(index, item);  // 顺序依然是 0,1,2
});

特点 2:跳过空位(稀疏数组)

javascript 复制代码
// 创建稀疏数组
const sparseArray = [1, , 3];  // 第二个元素是 empty
console.log(sparseArray.length);  // 3

sparseArray.forEach((item, index) => {
  console.log(index, item);
  // 输出:
  // 0 1
  // 2 3
  // 注意:索引 1 被跳过了
});

// 与 for 循环对比
for (let i = 0; i < sparseArray.length; i++) {
  console.log(i, sparseArray[i]);
  // 输出:
  // 0 1
  // 1 undefined  ← 这里不同!
  // 2 3
}

特点 3:在第一次调用前确定长度

ini 复制代码
const arr = [1, 2, 3, 4, 5];

arr.forEach((item, index, array) => {
  console.log(`处理 ${item}`);
  
  // 修改数组不会影响遍历次数
  if (index === 0) {
    array.push(6);   // 不会被处理
    array.pop();     // 不会减少处理次数
    array.length = 3; // 仍然会处理所有原始元素
  }
});

// 输出: 处理 1, 处理 2, 处理 3, 处理 4, 处理 5

4. 重要注意事项

⚠️ 注意点 1:无法中断

ini 复制代码
// ❌ 这些都无法中断 forEach
[1, 2, 3, 4, 5].forEach(item => {
  console.log(item);
  if (item === 3) {
    break;      // ❌ SyntaxError
    continue;   // ❌ SyntaxError
    return;     // ❌ 只退出当前回调,继续下一个
  }
});

// ✅ 替代方案
// 方案1: for...of
for (const item of [1, 2, 3, 4, 5]) {
  if (item === 3) break;
  console.log(item);
}

// 方案2: some() 或 every()
[1, 2, 3, 4, 5].some(item => {
  console.log(item);
  return item === 3;  // 返回 true 停止遍历
});

[1, 2, 3, 4, 5].every(item => {
  console.log(item);
  return item !== 3;  // 返回 false 停止遍历
});

⚠️ 注意点 2:异步处理问题

javascript 复制代码
// ❌ 异步操作不会按预期等待
async function processArray(arr) {
  console.log('开始');
  arr.forEach(async (item) => {
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log(item);
  });
  console.log('结束');  // 立即执行
}
// 输出: 开始 → 结束 → (100ms后) 1 2 3

// ✅ 替代方案
async function processArrayCorrectly(arr) {
  console.log('开始');
  
  // 方案1: for...of 配合 await
  for (const item of arr) {
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log(item);
  }
  
  // 方案2: 使用 Promise.all(并行)
  await Promise.all(arr.map(async (item) => {
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log(item);
  }));
  
  console.log('结束');
}

⚠️ 注意点 3:修改原数组的风险

ini 复制代码
const arr = [1, 2, 3, 4, 5];

// ❌ 危险:在遍历时添加/删除元素
arr.forEach((item, index, array) => {
  console.log(item);
  if (item === 2) {
    array.push(100);  // 可能导致意外行为
    array.splice(index, 1);  // 改变索引,可能跳过元素
  }
});

// ✅ 安全做法:先复制或使用其他方法
const arrCopy = [...arr];
arrCopy.forEach(item => {
  // 安全的处理
});

// 或者处理完后统一修改
const result = [];
arr.forEach(item => {
  result.push(item * 2);  // 不修改原数组
});

⚠️ 注意点 4:性能考虑

ini 复制代码
// forEach 与 for 循环的性能对比
const largeArray = new Array(1000000).fill(0);

console.time('forEach');
let sum1 = 0;
largeArray.forEach(num => {
  sum1 += num;
});
console.timeEnd('forEach');

console.time('for loop');
let sum2 = 0;
for (let i = 0; i < largeArray.length; i++) {
  sum2 += largeArray[i];
}
console.timeEnd('for loop');

// 结果:for 循环通常更快(但现代JS引擎优化很好)

5. 实际应用场景

场景 1:DOM 操作

javascript

javascript 复制代码
// 批量操作 DOM 元素
document.querySelectorAll('.items').forEach((element, index) => {
  element.classList.add('processed');
  element.dataset.index = index;
  element.addEventListener('click', handleClick);
});

场景 2:副作用操作

ini 复制代码
// 纯副作用,不需要返回值
const logs = [];
const data = [1, 2, 3];

// 记录日志
data.forEach(item => {
  logs.push(`Processing item ${item}`);
  console.log(`Processing: ${item}`);
});

// 更新外部状态
const cache = new Map();
const objects = [{id: 1}, {id: 2}];
objects.forEach(obj => {
  cache.set(obj.id, obj);
});

场景 3:链式操作的第一步

ini 复制代码
const result = [
  {name: 'Alice', age: 25},
  {name: 'Bob', age: 30},
  {name: 'Charlie', age: 25}
];

// forEach 作为预处理
const ageGroups = {};
result.forEach(person => {
  const age = person.age;
  if (!ageGroups[age]) {
    ageGroups[age] = [];
  }
});

// 然后进行其他操作
Object.keys(ageGroups).forEach(age => {
  ageGroups[age] = result.filter(p => p.age === parseInt(age));
});

6. 最佳实践

实践 1:明确使用意图

ini 复制代码
// ✅ 好的使用
// 1. 执行副作用
elements.forEach(el => el.classList.add('active'));

// 2. 更新外部状态
data.forEach(item => updateCache(item));

// 3. 简单的数据处理
items.forEach(item => console.log(item));

// ❌ 不好的使用
// 1. 需要返回值时(应用 map)
const doubled = [];
arr.forEach(x => doubled.push(x * 2));  // ❌ 用 map 更好

// 2. 需要过滤时(应用 filter)
const filtered = [];
arr.forEach(x => {
  if (x > 5) filtered.push(x);  // ❌ 用 filter 更好
});

实践 2:错误处理

javascript 复制代码
// ❌ 错误会中断整个遍历
try {
  [1, 2, 3].forEach(item => {
    throw new Error('Oops');
  });
} catch (e) {
  console.log('捕获到错误');  // 整个遍历停止
}

// ✅ 在回调内部处理错误
[1, 2, 3].forEach(item => {
  try {
    // 可能出错的操作
    riskyOperation(item);
  } catch (error) {
    console.error(`处理 ${item} 时出错:`, error);
    // 继续处理下一个
  }
});

实践 3:性能优化

ini 复制代码
// 提前缓存长度(虽然 forEach 内部会做,但某些情况有用)
const items = document.querySelectorAll('.item');
const itemsArray = Array.from(items);

// 批量操作减少重绘
const fragment = document.createDocumentFragment();
itemsArray.forEach(item => {
  const clone = item.cloneNode(true);
  // 修改 clone...
  fragment.appendChild(clone);
});
document.body.appendChild(fragment);

总结

✅ 使用 forEach 当:

  1. 只需要副作用,不关心返回值
  2. 操作顺序不重要,也不需要中断
  3. 代码可读性比极致性能更重要
  4. 处理 DOM 操作或 I/O 副作用

❌ 避免 forEach 当:

  1. 需要提前终止循环(用 for...ofsome/every
  2. 处理异步操作(用 for...ofPromise.all + map
  3. 需要转换数组(用 map
  4. 需要过滤数组(用 filter
  5. 需要计算结果(用 reduce
  6. 性能关键路径(考虑 for 循环)
  7. 需要链式调用(结合具体业务)
forEach 没有返回值,不修改原数组。当使用时可以给其他数组做中间方法
相关推荐
running up2 小时前
Vite 全面解析:特性、对比、实践及最新演进
javascript·typescript
.格子衫.2 小时前
JS原型链总结
开发语言·javascript·原型模式
shoubepatien2 小时前
JavaWeb_Web基础
java·开发语言·前端·数据库·intellij-idea
WordPress学习笔记3 小时前
wordpress外贸主题Google地图添加(替换)方案
前端·wordpress·wordpress地图
OrangeForce3 小时前
Monknow新标签页数据导出
javascript·edge浏览器
小妖6663 小时前
力扣(LeetCode)- 93. 复原 IP 地址(JavaScript)
javascript·tcp/ip·leetcode
码农秋3 小时前
Element Plus DatePicker 日期少一天问题:时区解析陷阱与解决方案
前端·vue.js·elementui·dayjs
未来之窗软件服务3 小时前
未来之窗昭和仙君(五十六)页面_预览模式——东方仙盟筑基期
前端·仙盟创梦ide·东方仙盟·昭和仙君·东方仙盟架构
top_designer3 小时前
Illustrato:钢笔工具“退休”了?Text to Vector 零基础矢量生成流
前端·ui·aigc·交互·ux·设计师·平面设计