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 当:
- 只需要副作用,不关心返回值
- 操作顺序不重要,也不需要中断
- 代码可读性比极致性能更重要
- 处理 DOM 操作或 I/O 副作用
❌ 避免 forEach 当:
- 需要提前终止循环(用
for...of或some/every) - 处理异步操作(用
for...of或Promise.all+map) - 需要转换数组(用
map) - 需要过滤数组(用
filter) - 需要计算结果(用
reduce) - 性能关键路径(考虑
for循环) - 需要链式调用(结合具体业务)