1. 识别代码坏味道的信号
长函数与高复杂度
在日常维护中,超过 40 行代码的函数常常隐藏着多种职责,这会显著提升理解成本。若一个函数需要在脑中追踪 多分支、条件与循环,就属于高复杂度的信号。
另一个关键点是 高圈复杂度(cyclomatic complexity),当度量数值上升,代码就变得难以测试、难以复用。此时,可维护性下降,并且后续修改容易引入副作用。
同时,长函数往往伴随重复片段和隐藏的副作用,如果一个职责改变需同时修改多处地方,就需要进行分解。
// before: 一个职责混合的函数
function renderUserProfile(user) {const name = user.name.trim();const age = new Date().getFullYear() - user.birthYear;const initials = name.charAt(0) + name.charAt(name.length - 1);const isAdult = age >= 18;const greeting = isAdult ? '你好' : '请注意';// 多个渲染分支与副作用const html = `${greeting}, ${name}(${age})`;document.getElementById('profile').innerHTML = html;// 更多逻辑…
}
做法要点:把大函数逐步拆成更小的职责单元,专注于单一职责原则;每次重构都明确一件事,减少认知负担。
通过识别上述信号,可以把重构目标定位为“分解功能”和“降低复杂度”,以便后续的提取和重用。
重复代码与潜在副作用
如果你在不同模块看到几段几乎相同的实现,这通常是重复代码的表现。重复会导致维护成本指数级上升,因为一个更改需要在多处同步。
另外,代码中存在隐性的副作用,比如全局变量的修改、对状态对象的直接变更,都会在未预期的路径触发错误。
重构的信号包括:提取共用逻辑、引入不可变数据结构、降低对全局状态的依赖等。
可读性与命名隐喻
可读性差的变量名和函数名,是另一种坏味道。若变量名仅仅是缩写,难以理解其语义,后续修改将极具挑战。
因此,命名要表达意图,不要把实现细节塞进名字里;另外,应避免把不同职责混放在同一个模块。
代码注释的信号
过多且模糊的注释通常是坏味道的标志。若注释需要解释商业逻辑或复杂的边界条件,说明代码本身对读者而言已不够直观。
优先的改进是让代码自解释,通过

2. 常用的重构技巧
提炼函数(Extract Function)
将长函数拆分成小函数,明确命名,可以让每个函数专注于单一职责。这样不仅提升可读性,也便于单元测试。
在逐步提炼时,先保留原有行为,再通过替换来实现分步演化;每次改动都应该通过测试来验证是否保持了行为一致。
// before: 业务逻辑混杂在一个函数中
function renderInvoice(items) {let total = 0;for (const it of items) {const lineTotal = it.price * it.qty;total += lineTotal;}const tax = total * 0.1;const grandTotal = total + tax;return { total, tax, grandTotal };
}// after: 提炼成更小的函数
function computeLineTotal(item) {return item.price * item.qty;
}
function computeSubtotal(items) {return items.reduce((sum, it) => sum + computeLineTotal(it), 0);
}
function computeTax(subtotal) {return subtotal * 0.1;
}
function renderInvoice(items) {const subtotal = computeSubtotal(items);const tax = computeTax(subtotal);const grandTotal = subtotal + tax;return { subtotal, tax, grandTotal };
}
提炼后的优势:职责分离、可复用性提高、测试覆盖更容易扩展。
消除重复(DRY原则)
重复代码会带来维护风险。通过将重复的逻辑抽象成独立的函数或模块,可以实现一次实现,多处复用。
在重构过程中,建立公共工具库或服务,将跨模块的公共逻辑统一管理,能显著降低后续修改成本。
// before: 两处类似的格式化逻辑
function formatDate1(d) {return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}`;
}
function formatDate2(d) {return `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}`;
}// after: 复用统一的日期格式化
function formatDate(d, fmt = 'YYYY-MM-DD') {const m = d.getMonth() + 1;const day = d.getDate();if (fmt === 'YYYY-MM-DD') return `${d.getFullYear()}-${m}-${day}`;if (fmt === 'YYYY/MM/DD') return `${d.getFullYear()}/${m}/${day}`;return d.toISOString().slice(0, 10);
}
解耦与模块化
将紧耦合的逻辑分离成独立的模块,可以降低模块间的依赖,提升可测试性和可维护性。重点在于定义清晰的接口和契约。模块边界要明确,避免模块内部实现细节暴露。
在前端应用中,使用 ES6 模块、工厂模式或简单的服务定位器,能够把状态、逻辑和UI职责分离,形成更易于演进的代码结构。
// before: 全部放在一个全局对象上
const App = {fetchUser: function(id) { /* ... */ },renderUser: function(u) { /* ... */ },saveUser: function(u) { /* ... */ }
};// after: 采用模块化组织
// userService.js
export function fetchUser(id) { /* ... */ }// uiRenderer.js
export function renderUser(user) { /* ... */ }// app.js
import { fetchUser } from './userService.js';
import { renderUser } from './uiRenderer.js';
async function showUser(id) {const user = await fetchUser(id);renderUser(user);
}
将命名空间与可维护性结合
合理的命名空间可以减少全局污染并提升代码可读性。通过封装私有实现、暴露清晰接口,实现更易于测试和演进的代码基础。
在团队协作中,遵循一致的命名约定和模块约束,能显著降低沟通成本和误解风险。
3. 从测试驱动到高质量改进的流程
从测试驱动重构开始
在大规模重构前,建立健全的测试体系是关键:单元测试、集成测试与端到端测试应覆盖核心行为和边界情况。
实践要点包括:先写测试再改动、小步提交、可回滚,以及通过 CI 验证每次变更的稳定性,以降低风险。
// Jest 测试示例
function computeTotal(items) {return items.reduce((sum, it) => sum + it.price * it.qty, 0);
}
describe('computeTotal', () => {it('returns correct total for single item', () => {expect(computeTotal([{ price: 5, qty: 2 }])).toBe(10);});it('handles multiple items', () => {expect(computeTotal([{ price: 3, qty: 1 }, { price: 4, qty: 2 }])).toBe(11);});
});
测试驱动的目标是确保你的重构没有破坏既有行为,同时让改动更可追踪和可验证。
渐进式变更与回滚策略
逐步改动比一次性大改要安全得多:分支开发、功能标记(feature flag)与分阶段合并是常用策略。
同时,保留可回滚的历史记录,在遇到不可预期的问题时能够快速回退,减少系统被动停机时间。
# Git 工作流示例
git checkout -b refactor/compute-total
# 提交逐步改动
git commit -m "提炼 computeTotal 为独立函数"
# 使用 feature flag 逐步启用新实现
4. 实战中的JavaScript重构案例
模块化与命名空间重构
在較早的项目中,常见的工具和逻辑堆积在全局命名空间中。将它们拆分成独立的模块,不仅提升可维护性,也让单元测试变得容易。
示例中,通过把工具函数与业务逻辑分离为不同的模块,并在入口处注入依赖,代码的灵活性显著提升。
// before: 全局暴露函数
var Utils = {formatDate: function(d) { /*...*/ },fetchData: function(url) { /*...*/ }
};// after: 使用模块化组织
// utils.js
export function formatDate(d) { /*...*/ }
export function fetchData(url) { /*...*/ }// main.js
import { formatDate, fetchData } from './utils.js';
console.log(formatDate(new Date()));
使用Promise/async/await改写回调地狱
回调地狱会让代码可读性迅速下降。通过Promise、async/await的组合,可以把异步流程写成接近同步的结构,提升可维护性。
改写的核心是:将回调转为 Promise 接口,并在调用端使用 async/await 处理顺序与并发。
// before: 回调风格
fs.readFile('/path/data.json', function(err, data) {if (err) return handleError(err);processData(data, function(err, result) {if (err) return handleError(err);render(result);});
});// after: Promise + async/await
function readData(path) {return new Promise((resolve, reject) => {fs.readFile(path, (err, data) => err ? reject(err) : resolve(data));});
}
async function main() {try {const data = await readData('/path/data.json');const result = await processData(data);render(result);} catch (err) {handleError(err);}
}
5. 常见陷阱与最佳实践
避免过度重构
重构并非越多越好,若改动没有带来实质性的可维护性提升,避免过度重构,以免陷入“改动披风下的无效改造”。
在决策时,关注 可读性、稳定性与长期维护成本,而不是仅仅追求代码形式上的干净。
// 避免的陷阱:在一个小模块内反复微小改动,导致代码风格混乱。
保持可读性与性能的平衡
重构时应始终以提升可读性为前提,而不是单纯追求性能最优。合理的选择是通过更清晰的结构和更简洁的算法来提升长期性能,而不是过早优化。
对性能敏感的路径,可以在确保正确性后再进行优化,避免在初期引入复杂度以换取短期的微小收益。
// 优化建议:先让代码更易懂,再评估性能瓶颈
function fetchAndRenderUsers() {return fetch('/api/users').then(res => res.json()).then(users => users.filter(u => u.active)).then(activeUsers => render(activeUsers));
}


