重构代码会出问题吗?别急着删分支,先看看这几种翻车现场

上周帮同事看一个上线后报错的页面,查了半天发现是前两天他刚“优化”过的登录逻辑——把原来的三个 if 判断合并成一个三元表达式,结果用户名为空时直接返回了 undefined,前端调用时炸了。他挠头说:“我就是重构一下,又没改功能……”

重构不是重写,但真可能出问题

很多人以为“重构 = 换种写法”,只要跑得通就万事大吉。其实重构是在不改变外部行为的前提下调整内部结构。可现实里,稍不留神,“不改变行为”这句承诺就失效了。

常见翻车点一:漏掉边界条件

比如原代码里有个判断:

if (user.age >= 18 && user.age <= 65) {
return 'eligible';
} else {
return 'not eligible';
}

重构时图省事改成:

return user.age >= 18 && user.age <= 65 ? 'eligible' : 'not eligible';

看起来一样?但如果 user.age 是 null 或字符串 'unknown',原代码会走 else 分支返回 'not eligible';新写法在 && 过程中遇到 null 就短路返回 false,结果还是 'not eligible'——暂时没问题。但万一哪天后台开始传 NaN 呢?NaN >= 18 是 false,NaN <= 65 也是 false,结果还是 false,看似稳……可如果后续加了日志埋点,依赖 age 的原始值做统计,那 NaN 被静默吞掉,数据就歪了。

常见翻车点二:this 或作用域悄悄变了

ES5 里把函数抽成独立方法,最容易丢 this:

// 原来在对象方法里
handleClick() {
console.log(this.userId); // 有值
setTimeout(function() {
console.log(this.userId); // undefined!
}, 100);
}

重构时改成箭头函数或 bind,看着清爽了,但如果组件被其他库劫持过上下文(比如某些老版 React HOC),this 可能早不是你以为的那个 this。

常见翻车点三:依赖未声明的隐式行为

有次修一个表单提交慢的问题,发现提交前有段“兼容旧版”的空循环:

// 注释写着“等 legacy service 初始化”
for (let i = 0; i < 1000; i++) {
if (window.legacyReady) break;
await new Promise(r => setTimeout(r, 1));
}

重构时觉得这太丑,直接删了——结果第二天运营说后台导出 Excel 功能全失败。查了一圈才发现,那个 legacy service 不光要初始化,还得靠这个循环“触发一次心跳”,否则后端认为连接断开,拒绝接收后续请求。

怎么降低重构翻车概率?

不是不能重构,而是得带“安全带”:
• 提交前跑一遍已有测试(没有?现在补一个最简单的也比没有强);
• 对关键路径,用 diff 工具对比重构前后接口返回的 JSON 字段、类型、嵌套深度;
• 在测试环境用真实用户流量镜像压测几小时,看错误率有没有跳变;
• 改完别急着合主干,先在小流量灰度发 5%,盯着监控里的 4xx/5xx 和 JS error 日志。

重构本身没错,错的是把“没报错”当成“没毛病”。代码跑得通,和它在所有场景下都按预期工作,中间差着一整个生产环境的距离。