数组 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 没有返回值,不修改原数组。当使用时可以给其他数组做中间方法
相关推荐
Live0000034 分钟前
在鸿蒙中使用 Repeat 渲染嵌套列表,修改内层列表的一个元素,页面不会更新
前端·javascript·react native
柳杉35 分钟前
使用Ai从零开发智慧水利态势感知大屏(开源)
前端·javascript·数据可视化
兆子龙1 小时前
从高阶函数到 Hooks:React 如何减轻开发者的心智负担(含 Demo + ahooks 推荐)
前端
狗胜1 小时前
测试文章 - API抓取
前端
三小河1 小时前
VS Code 集成 claude-code 教程:告别海外限制,无缝对接国内大模型
前端·程序员
jerrywus1 小时前
前端老哥的救命稻草:用 Obsidian 搞定 Claude Code 的「金鱼记忆」
前端·agent·claude
球球pick小樱花1 小时前
游戏官网前端工具库:海内外案例解析
前端·javascript·css
用户60572374873081 小时前
AI 编码助手的规范驱动开发 - OpenSpec 初探
前端·后端·程序员
狗胜1 小时前
AI观察日记 2026-03-02|CLAUDE、TYPE、APPFUNCTIONS:掘金热门里的下一步信号
前端