
你的代码不是写得烂,是「写得急」——9 个补救动作照着改
先还原一个咱们都经历过的场景。
周四下午,产品在群里 @你:「这个需求今晚能上不?运营等着明天发券。」你看了眼,逻辑不复杂,加个判断的事儿。于是你复制了隔壁一段相似的代码,改了两个值,变量懒得重命名,data、temp、result 先顶上,心里默念一句「回头再收拾」,提交,上线,收工。
回头,永远没来。
三个月后的凌晨两点,你的手机响了。线上支付在报错,运营群已经炸了,值班的同事——可能是你,也可能是接手你项目的新人——打开那个函数:180 行,变量还是 data 和 temp,每个分支都在改同一个共享状态。代码是能跑的,但没人知道它为什么能跑,更没人敢动它。
这就是难读代码真正的代价:它不在你写下的那一刻爆炸,它在别人要快速、安全、又没有你脑子里那点上下文的情况下去改它的那一刻爆炸。
而且说句公道话——大多数难读的代码,真不是烂程序员写的,是被 deadline 追着跑的好程序员写的。所以这篇不批判谁,只给补救。我把 review 时最常打回的问题,整理成 9 个能直接照着改的动作,每个都配一个咱们熟悉的业务场景。你可以对着自己手上的项目,一条条过。
补救 1:让代码自己说「这是激活用户」
场景:用户中心要筛「可下单的活跃用户」,发券、发短信都调它。
当时你三分钟写完了这行:
javascript// ❌ 三个月后接手的人:s 是啥?1 为什么是激活?b 又是啥?
const x = users.filter((u) => u.s === 1 && u.r !== 'b');问题不在这行短,而在于读的人得去翻类型定义、翻数据库枚举、翻你三个月前的提交,才敢确认 s === 1 是「激活」、r === 'b' 是「封禁」。营销活动要复用这段逻辑时,没人敢直接拿去用——万一 1 不是激活呢?
改法很便宜,就是把业务规则从你脑子里挪进代码里:
javascript// ✅ 谁读都不用猜
const activeCustomers = users.filter(
(user) => user.isActive && user.role !== UserRole.Banned,
);编译器根本不在乎变量叫 x 还是 activeCustomers。但 code review 的同事、排障的值班、下个月接手活动的新人,全都在乎。好命名的本质,是替后面每一个读代码的人,省掉一连串「这是啥意思」的提问。
补救 2:别让「炫技」拖住半夜排障的人
场景:订单列表要按类型分组,你想秀一把,一行搞定。
javascript// ❌ 一行塞了 5 个语言细节:表达式内赋值、??=、变异、逗号运算符、reduce
const result = itemList.reduce(
(a, x) => ((a[x.type] ??= []).push(x), a),
{},
);写的时候很爽,十行压成一行,同事路过还夸一句「牛」。但真到线上订单分组出问题、有人半夜来 debug 时,他得同时在脑子里跑通这五个细节,才敢下手。这一行省下的时间,全加倍还给了排障的人。
换成朴素写法,多几行,但谁都能改:
javascript// ✅ 无聊,但一眼看懂
const ordersByType = {};
for (const order of orderList) {
if (!ordersByType[order.type]) {
ordersByType[order.type] = [];
}
ordersByType[order.type].push(order);
}不是说 map/filter/reduce 就有罪——团队都熟的写法,紧凑照样好读。坏就坏在**「显得高级」压过了「让人看懂」**。生产代码不是炫技比赛,它是全组共用的操作系统。别让接盘的人先破译你的智商,才能修那个 bug。
补救 3:那个 200 行的「下单函数」,该拆了
场景:placeOrder 一个函数从头干到尾——校验、查权限、算价、用券、扣款、发邮件、写日志、拼返回。
javascript// ❌ 加了注释「看起来」有条理,架构还是一团麻
async function placeOrder(req, res) {
// 校验请求
// 查用户权限
// 算商品价格
// 应用优惠券
// 调支付扣款
// 保存订单
// 发确认邮件
// 写审计日志
// 返回结果
}它的问题不在长,在于背了太多「要改的理由」:支付一挂,你得读懂整个下单流程;双十一改价格策略,改动就贴着支付代码,一不小心动到扣款;哪天邮件要改异步,同一个函数又得动。每一次需求变更,都在同一个火药桶上点火。
改法不是机械拆成一堆两行小函数,而是按有意义的业务决策来分:
javascript// ✅ 主流程读起来像一句人话
const order = await buildOrder(command); // 校验+算价+用券,都在这后面
await paymentService.charge(order.payment); // 支付只管支付
await orderRepository.save(order);
await orderEvents.publishCreated(order); // 发邮件/写日志走事件,解耦细节还在,只是躲到了「能解释它为什么存在」的名字后面。以后价格逻辑要改,你只进 buildOrder;支付出问题,你只看 paymentService。小函数在暴露关键概念时有用;但每两行就抽个 handleStuff(),那是噪音。目标不是更少的代码,是每个单元背更少的责任。
补救 4:注释别翻译代码,要留「当时为什么这么干」
场景:对接第三方支付的 webhook 回调。
先看一种最没用的注释——把代码用中文又念一遍:
javascript// ❌ 检查用户是否激活 ← 这句等于没说
if (user.status === 1) {它不如直接把代码改得不用注释:
javascript// ✅
if (user.isActive) {但真正保命的注释,是记下代码本身表达不了的决定:
javascript// 支付方在网络抖动时会重发同一个 webhook,
// 处理前先用事件 ID 去重,否则用户会被重复扣款。
if (await hasProcessed(event.id)) return;这条注释拦住的是一场事故。没有它,某个热心同事下次「优化」时,一眼觉得这个去重判断多余,删了——然后大促当天,重发的 webhook 让一批用户被扣了两次款,你又得凌晨爬起来。
对接外部服务、历史遗留兼容、反常的性能取舍、乍看不合理其实有原因的业务规则——这些地方的注释最值钱。记住:代码解释「做了什么」,注释解释「为什么非这么做不可」。
补救 5:调用处一排 true, false,鬼才看得懂
场景:生成对账报表,函数支持含税、导 PDF、发邮件等一堆开关。
javascript// ❌ true 是含税?导 PDF?发邮件?用缓存?得翻定义数参数
generateReport(data, true, false);半年后运营提需求:「报表能不能不发邮件」,接手的人盯着这行 true, false 完全不敢动,生怕改错一个位置把含税关了。多个位置参数是同一个病,加个字段就更乱:
javascript// ❌ 含义全靠位置
createUser(name, email, role, true, false, null);给它们起名,决策就摆在了调用处:
javascript// ✅ 一眼看懂每个开关是干嘛的
generateReport(data, {
includeTax: true,
sendEmail: false,
});
createUser({
name,
email,
role,
isVerified: true,
sendWelcomeEmail: false,
});不是每个函数都要上对象参数——add(2, 3) 挺好。当参数代表配置、业务开关、或多个同类型的值时,再上命名选项。一次函数调用,光看调用处就该读懂决策,不用打开第二个文件去对照。
补救 6:会员权限判断,别再套四层 if
场景:判断用户能不能进会员专享页——要登录、要激活、要有订阅、订阅没过期。
javascript// ❌ 读到最里层,脑子里还压着四个条件
if (user) {
if (user.isActive) {
if (user.subscription) {
if (!user.subscription.expired) {
return showDashboard(user);
}
}
}
}这种金字塔,加需求时最痛——产品说「过期的也让进,但弹个续费引导」,你得数着大括号找该在哪插分支,还容易插错层。用卫语句把非法情况在开头挡掉,主流程立刻就平了:
javascript// ✅ 无效情况早退,还能给出具体理由
if (!user) {
throw new NotFoundException('用户不存在');
}
if (!user.isActive) {
throw new ForbiddenException('用户已被禁用');
}
if (!user.subscription || user.subscription.expired) {
return showUpgradePage(user); // 过期→引导续费,理由明确
}
return showDashboard(user);一张图看这个「拍平」,左边越读越背状态,右边逐个拦截、每步都有明确理由:
code嵌套写法 卫语句写法
───────── ──────────
if (user) { if (!user) → 抛「不存在」
if (isActive) { if (!active) → 抛「被禁用」
if (subscription) { if (过期) → 跳续费页
if (!expired) { ───────────────────────
showDashboard() return showDashboard()
} ← happy path 就一行,是平的
}
}
}
读到底:脑中挂 4 个条件 🤯 读到底:0 负担 😌卫语句不是信仰,状态流转特别复杂时早返回太多也会乱。它最擅长的是在函数开头挡掉非法入参、缺失数据、越权、不支持的状态。一句话:读的人不该为了看懂最后一行,先在脑子里装下半个函数。
补救 7:一个团队,别让每个人发明自己的方言
场景:三个人分头写三个 service,风格各飞各的。
一个接口报错抛异常,另一个返回 null,第三个返回 { success: false };用户表里一会儿 createdAt、一会儿 created_at;同一个「查用户」操作,这边 getUser、那边 fetchUser、还有 loadUser 和 findUser。
每处差异都小,加一起,就逼着新人一个文件一个文件重新学这套系统——每读一个模块都要先问「这个返回啥?出错抛还是不抛?」。可读的代码,是可预测的代码。
约定好:仓储方法要么返回实体、要么抛已知错误,就全程守住;API 返回遵守同一份契约,别让某个接口自创形状;命名风格统一,就别中途换词。这不是要把烂约定供着——坏模式该改,但要有意、且协调好地改。一个代码库里两套打架的约定,往往比一套不完美的约定更折磨人。名字具体叫什么不重要,大家一致才重要:让人能靠读懂熟悉的代码,去预测没读过的代码。
补救 8:catch (e) { console.log(e) } 说的是不是你
场景:下单里调支付,包了个 try-catch,「先把错误吞了别让页面崩」。
javascript// ❌ 藏住了失败,调用方拿不到任何信号
try {
await processPayment(order);
} catch (error) {
console.log(error);
}于是真出事那天,你面对的是这些问题,一个都答不上来:支付是在支付方接受之前挂的,还是之后?这单会不会重试?订单现在是不是还挂着?日志里有订单号能让我捞出来吗?——啥都没有,只有一行漂在控制台里、早被冲走的 error。
给失败一个结构,让它和成功路径一样「是设计出来的」:
javascript// ✅ 出事时,现场留得下线索
try {
await paymentService.charge(order);
} catch (error) {
logger.error('支付扣款失败', {
orderId: order.id,
customerId: order.customerId,
error,
});
throw new PaymentFailedError(order.id); // 往上抛明确的错误,让调用方决定重试还是回滚
}可读的错误处理,告诉人:什么会失败、谁负责兜底、出事时现场会留下什么。「系统繁忙请稍后再试」糊弄用户还行,对凌晨排障的人毫无用处。内部错误该带上稳定错误码、请求 ID、订单/用户标识和上下文——但别把密钥、token 也打进日志。一个只解释成功的系统,是没法被理解的。
补救 9:测试要写「行为」,不是写「调了哪个方法」
场景:还是那个重发 webhook 的幂等逻辑,你想加个测试锁住它。
javascript// ✅ 这条测试在替你「说明」:webhook 处理必须幂等
it('同一个支付回调不会给用户扣两次款', async () => {
await handlePaymentWebhook(event);
await handlePaymentWebhook(event); // 故意重发一次
expect(paymentProvider.charge).toHaveBeenCalledTimes(1);
});半年后有人来改这块逻辑,一看测试名就懂了红线在哪——「哦,这里不能重复扣款」。对比一条只盯实现细节的弱测试:
javascript// ❌ 只证明「方法被调了」,没说清「为什么这行为重要」
expect(repository.findById).toHaveBeenCalled();
expect(repository.save).toHaveBeenCalled();它保护不了任何业务约束,重构一动就红,还看不出到底哪儿错了。好测试用有意义的名字、把数据摆清楚、盯可观测的行为,把那些容易从记忆里蒸发的决定钉死。一条好测试,能让人比读完整个 service 更快看懂这个功能——它就是一份能跑起来的说明书。
说到底:可读,是团队的事,不是个人品味
一个人救不活整个代码库。可读的系统靠共识:命名约定、格式化规则、review 习惯、错误契约、目录结构、测试范式。
Code review 该问的,不止「这能跑吗」,而是——不看需求单能读懂意图吗?名字够具体吗?这函数是不是揽太多活了?失败路径看得见吗?新人知道该去哪改吗?这层抽象是在降复杂度,还是在藏复杂度?
也别走极端。生产团队真有 deadline,有时正确的选择就是先发一版清楚但略啰嗦的实现,而不是为一个优雅抽象拖住上线。回到开头那句——你的代码不是写得烂,是写得急。急没关系,但欠下的账,找个不那么急的下午,照着上面 9 条补回来。
好代码不证明你当年多聪明。它让下一个人不用把你从被窝里捞起来,就能改。写那种在你的记忆、你的需求单、你的口头解释都消失之后,还能活下去的代码。
如果这篇对你有帮助,欢迎关注公众号「前端达人」,每周更新实用前端干货。

