先记住这个答案
for...of 是一个循环语句,可以用 continue 跳过当前项、break 结束循环,写在函数里时 return 还会直接退出整个函数。forEach 是一个方法,它给每个元素调用一次回调,回调里的 return 只是结束这一次回调,下一个元素照常处理,所以 forEach 没有正常的方式提前结束。异步场景差别更大:forEach 不会等 async 回调返回的 Promise,需要逐个等待就用 for...of 配 await,需要并发就收集 Promise 交给 Promise.all。
forEach里return等于continue- 提前结束用
break,或改用some、find forEach不等async回调,await它没用- 逐个等待用
for...of,并发用Promise.all
同样想在 5 停下,结果不一样
需求是:跳过小于 3 的数,遇到 5 就停止。分别用两种写法实现。
break 和回调里的 returnJavaScriptconst values = [1, 3, 5, 6]
const loop = []
for (const value of values) {
if (value < 3) continue // 跳过当前项
if (value === 5) break // 结束整个循环
loop.push(value)
}
const callbacks = []
values.forEach(value => {
if (value < 3 || value === 5) return // 只结束这一次回调
callbacks.push(value)
})
console.log(JSON.stringify(loop))
console.log(JSON.stringify(callbacks))查看输出与解释
[3]
[3,6]for...of 在 5 处 break,6 没有被处理。forEach 在 5 那次回调里 return,只是跳过了 5,接着又为 6 调用了回调。原因在于 return 退出的是离它最近的那个函数,在 forEach 里,这个函数就是回调,而不是外面的遍历。
所以 forEach 里的 return 效果等于 continue,它没有对应 break 的写法。
需要提前结束时换成哪个方法
要提前结束遍历,除了 for...of,数组本身就有几个会「命中即停」的方法。判断有没有符合条件的元素用 some,回调返回 true 就停;要拿到那个元素用 find,要拿索引用 findIndex;判断是不是全部满足用 every,回调返回 false 就停。
- 不要在
forEach外面加标志位 - 有人会在
forEach外面定义一个found变量,找到后置为true,后面的回调一进来就return。这样结果是对的,但遍历并没有停,剩下的元素还是会逐个调用回调,只是什么都不做。代码意图也更难看懂,直接用some或for...of更清楚。
forEach 不会等 async 回调
forEach 调用回调后不管返回值是什么,回调是 async 函数时返回的是一个 Promise,forEach 直接丢掉它,马上调用下一个回调,最后返回 undefined。所以 await arr.forEach(async ...) 等的是 undefined,什么也没等到,回调里的错误也没法被外层 try...catch 捕获。
- 顺序执行和并发执行的写法
- 需要一个接一个执行,比如按顺序上传分片,就用
for (const item of list) { await upload(item) }。任务之间互不依赖,想一起发出去,就用await Promise.all(list.map(item => upload(item)))。数量很大时一次全发出去会压垮服务端,要分批或者限制并发数。
容易答错的地方
- 以为
forEach里return false能像 jQuery 的each一样停止 - jQuery 的
$.each确实约定返回false就停止,但原生forEach完全忽略回调的返回值,返回什么都不会停。 - 说
for...of和forEach对空位的处理一样 - 稀疏数组
[1, , 3]中间有个空位。forEach会跳过空位,只调用两次回调;for...of通过迭代器按索引读取,空位读到的是undefined,会循环三次。
面试官还会怎么问?
for...of 里的 return 只是退出循环吗?
不是。for...of 不是函数,return 退出的是包着它的那个函数,循环后面的代码也不会执行了。只想结束循环、继续执行后面的代码,要用 break。
for...of 能遍历普通对象吗?
不能,普通对象没有实现迭代器,会报 TypeError。可以先转成数组再遍历,比如 for (const [key, value] of Object.entries(obj))。遍历对象的键还有 for...in,但它会把原型链上可枚举的属性也遍历出来。
for...of 和 forEach 哪个性能更好?
日常数据量下差别可以忽略,选哪个应该看需不需要 break 和 await。真遇到性能热点时,普通 for 循环通常最快,但也要在目标环境里实测再下结论。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。