2023年Q3,某电商大促期间,一段for循环把支付服务拖垮,直接损失订单流水2700万。问题代码只有7行,罪魁祸首是个被遗忘的i++。
循环这东西,写代码的第一天就学了,但栽跟头的高级工程师年年有。今天把JavaScript循环的坑扒清楚——不是基础教程,是事故复盘。
for循环:最熟悉的陌生人
for循环的语法像地铁换乘指示牌:起点、终点、每站怎么走,三句话说完。但90%的线上事故出在第四件事——循环变量的作用域。
ES6之前,var声明的循环变量会穿透块级作用域。看这段经典面试题:
for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); }
输出不是0、1、2、3、4,是5个5。var的i在全局挂着,等setTimeout执行时,循环早跑完了,i定格在5。2015年前这个坑埋了无数前端,现在用let能解,但老代码库里还躺着定时炸弹。
另一个高频事故点:循环条件写死成true,或者忘记更新变量。这就是无限循环——CPU占用飙到100%,服务卡死,日志狂刷直到磁盘爆满。某厂监控告警规则里,"单进程CPU>90%持续30秒"自动kill,就是为了防这个。
while与do-while:条件判断的陷阱
while先验票后上车,do-while先上车后验票。差别看着细,实际决定了一次执行还是零次执行。
某金融系统曾出过事:用while读取数据库批量任务,结果表空了,条件一开始就false,任务队列直接跳过。换成do-while至少执行一次校验,能发现空表异常。产品经理后来复盘说,"这就像电梯门感应,没检测到人也得关一次门才知道"——有点怪,但确实是那个理。
do-while在真实业务里出场率不到5%,但特定场景不可替代。比如用户输入校验,至少得问一次"你确定吗",不能上来就判断。
for...in与for...of:对象遍历的雷区
这俩名字差一个字母,行为天差地别。for...in遍历可枚举属性,包括原型链上的;for...of遍历可迭代对象的值。
用for...in数组是新手经典错误。数组也是对象,索引是属性名,for...in会把"0"、"1"、"2"当字符串吐出来,还可能混进Array.prototype上挂的自定义方法。2019年某开源库就在生产环境踩过这坑,遍历数组时意外执行了某人挂载的polyfill,数据全乱。
for...of需要对象实现迭代器协议(Symbol.iterator)。普通对象直接用会报错,得先用Object.keys()或Object.entries()转一手。ES2017引入的Object.values()、Object.entries()就是干这个的。
性能暗战:哪种循环最快?
V8引擎对循环做过深度优化,但写法不同,执行效率能差出数量级。
传统for循环(i < arr.length)每次迭代都要访问length属性。数组长度固定时,缓存到变量里(var len = arr.length)能省一次属性查找。实测大数据量下,缓存版比原始版快15%-30%,具体看V8版本。
forEach、map、filter这些数组方法写着爽,但函数调用开销摆在那。Chrome 90之后V8做了优化,差距缩小到10%以内,但极端性能场景(比如实时音视频处理),裸for还是首选。
reduce是函数式编程的心头好,但调试地狱。堆栈报错时,你看到的是reduce内部的匿名函数,不是业务代码位置。某团队规范直接写明:"涉及金额计算禁用reduce,出错后审计追不到行号"。
async/await进循环:顺序与并行的抉择
循环里塞异步操作是 modern JS 的必修课,但forEach+async是经典翻车组合。
forEach不会等待Promise解决,回调里的async函数全抛出去各自跑,原函数早就return了。想要顺序执行,得用for...of配合await,或者退回到传统for循环。要并行执行,用Promise.all包一层map。
2022年某爬虫项目因此吃了官司:用forEach发请求,没等响应就标记任务完成,结果丢数据被甲方追责。技术负责人后来写进wiki——"forEach和async结婚,生的是孤儿Promise"。
Node.js的event loop遇上同步无限循环更危险。while(true)会阻塞整个线程,后续的I/O回调全饿死。这就是为什么要用setImmediate或process.nextTick拆分任务,或者干脆上worker_threads。
循环是编程的基石,但基石下面常有暗河。你最近一次排查循环相关bug,花了多久定位到问题?
热门跟贴