如何判断你的函数是否"只做了一件事"?
用三个实用标准判断函数是否符合单一职责原则
在代码审查(Code Review)中,我们最常听到的建议之一就是:"这个函数太大了,拆分一下。" 或者更学术一点的说法:"违反了单一职责原则(SRP)。"
Robert C. Martin 在《代码整洁之道》中有一句名言:
"函数应该做一件事。做好这件事。只做这件事。"
这句话听起来像真理,但在实际的前端开发中却很难落地。比如一个"提交表单"的按钮点击事件,它似乎天生就要做很多事:校验数据、开启 Loading 状态、发送请求、处理报错、跳转路由……这难道不是"一件事"吗?
今天我们就从前端的视角出发,用三个实用的标准来判断:你的函数到底是在做一件事,还是在做一锅粥?
标准一:能否用"TO"段落清晰描述?
这是最直观的判断方法。尝试用自然语言描述这个函数在做什么,如果你的描述中出现了 "并且 (AND)"、"或者 (OR)" 这样的连接词,那么它很可能做了不止一件事。
❌ 反面教材
假设我们有一个 handleLogin 函数:
"要 (TO) 处理登录,我们需要校验表单数据,并且如果有错误就显示错误提示,否则开启 Loading 状态,并且发送 API 请求,并且将返回的 Token 存入 localStorage,最后跳转到首页。"
这一连串的"并且",说明这个函数承载了:校验逻辑 + UI 状态管理 + 网络请求 + 数据持久化 + 路由控制。
✅ 正面例子
重构后的 handleLogin 应该像是一个"指挥官",而不是"搬砖工":
"要 (TO) 处理登录,我们执行登录流程,成功后重定向。"
代码可能长这样:
// 这是一个只做一件事的函数(它只负责指挥)
async function handleLogin(formData) {
if (!isValid(formData)) return;
try {
showLoading();
await loginService.login(formData);
redirectToHome();
} catch (error) {
showErrorToast(error);
} finally {
hideLoading();
}
}注意,虽然它调用了多个函数,但它本身只做了一件事:协调登录流程。具体的校验、请求、存储细节,都交给了下层函数去完成。
标准二:抽象层级是否一致?(The Abstraction Level Test)
这是前端代码最容易踩的坑。"做一件事"并不意味着函数只能有一行代码,而是意味着函数内的所有步骤都在同一个抽象层级上。
想象一个公司的组织架构:CEO 负责制定战略(高层级),经理负责分配任务(中层级),员工负责具体干活(低层级)。如果一个函数里既有"战略决策",又有"拧螺丝"的代码,那就是混合抽象层级,它一定做了不止一件事。
🔍 前端视角的混合层级
看看下面这段 React 代码:
// ❌ 混合了不同层级
const UserProfile = () => {
// ...
const fetchUser = async () => {
// 高层语义:获取用户
// 低层实现细节:具体的 fetch 配置、Header 拼接
const token = localStorage.getItem('token');
const res = await fetch('/api/user', {
headers: { 'Authorization': `Bearer ${token}` }
});
// 低层实现细节:手动解析 JSON
const data = await res.json();
// 高层语义:设置数据
setUser(data);
};
// ...
}在这个 fetchUser 函数中:
- 高层概念:我要获取用户。
- 低层细节:HTTP Header 怎么拼、LocalStorage 的 Key 是什么、JSON 怎么解析。
判断技巧: 如果你的函数里同时出现了 businessLogic()(业务逻辑)和 element.style.color = 'red'(原生 DOM 操作)或 headers: {}(底层配置),那就是层级混乱。
✅ 修正后
const fetchUser = async () => {
// 全是高层级,只做一件事:调用服务并更新状态
const data = await userApi.getUser();
setUser(data);
};标准三:代码块是否在"视觉上"分组?
当你阅读一个长函数时,如果你发现自己下意识地想在某些行之间加空行,或者想给某几行代码加个注释来说明"这部分是在初始化数据"、"这部分是在处理 DOM",那么这些"块"就暗示了它们应该被提取出去。
场景:由 useEffect 引发的"大杂烩"
React 的 useEffect 是重灾区。很多时候我们会在一个 Effect 里做太多事。
// ❌ 视觉上明显分成了三块
useEffect(() => {
// 块1:订阅窗口大小变化
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 块2:埋点上报
analytics.track('page_view');
// 块3:初始化数据
fetchData();
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);虽然它们都发生在"组件挂载时",但在逻辑上它们是三件完全不相干的事。这不仅违反了单一职责,还让依赖项数组(Dependency Array)变得极其难以管理。
修正: 拆分成三个独立的 useEffect,或者自定义 Hooks (useWindowSize, useTracking, useDataFetching)。
总结:前端"单一职责"实战心法
要判断你的函数是否只做了一件事,请问自己三个问题:
- 描述测试:我能用一句不带"并且"的话描述它吗?
- 层级测试:我是不是在同一个函数里既写了业务逻辑,又写了
document.querySelector或fetch配置? - 视觉测试:我是否需要用空行和注释把代码分成好几块?
记住: 我们将大函数拆小的目的,不是为了增加文件数量,而是为了将"做什么(What)"和"怎么做(How)"分离开来。当你把所有的"怎么做(细节)"都藏在小函数里,你的主函数就会像一篇清晰的文章,只告诉你它在"做什么"。
这才是代码整洁之道的精髓。