数组 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 没有返回值,不修改原数组。当使用时可以给其他数组做中间方法
相关推荐
hpoenixf20 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特20 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷20 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian21 小时前
前端node常用配置
前端
华洛21 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq21 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A1 天前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常1 天前
被EdgeToEdge适配折磨疯了,谁懂!
前端
小码哥_常1 天前
从Groovy到KTS:Android Gradle脚本的华丽转身
前端
灵感__idea1 天前
Hello 算法:复杂问题的应对策略
前端·javascript·算法