CR 只看格式命名,是初级信号。完整 CR 有 5 层:格式、语法逻辑、异常副作用、架构可维护性、线上性能安全。答到第三层才算合格。
一个问题就能分层
上周面了个 4 年前端。简历写着"熟悉团队代码规范,经常参与 CodeReview"。
我问:你们做 CR 主要看什么?
他脱口而出:格式对不对,命名好不好看。
我追问:如果现在让你做一次完整的 CodeReview,从业务逻辑、边界处理到可维护性、线上风险,你能说出多少个检查维度?不同缺陷的严重优先级怎么区分?
他卡住了。
很多前端以为 Code Review 就是改空格和变量命名。到 2026 年,代码规范和 CR 早不只是格式化代码,这是工程能力的分水岭。
我一般用 5 个层面测人。第三层算合格,第四第五层是高级开发和普通开发的差距。
五层全景
先把全景摆出来,后面逐层拆。
L1 格式规范:别把工具活当技术活
命名、缩进、分号、引号统一、禁止未定义变量------最没技术含量,也最容易被误当成 CR 的全部。
真正的追问在后面:同样是 ESLint 报错,哪些必须阻断合并,哪些只做警告提示?自动 Fix 过后,会不会隐藏真正的 bug?
看配置就明白:
js
// .eslintrc.cjs ------ 区分"阻断"和"提示"
module.exports = {
rules: {
'no-unused-vars': 'error', // 阻断:可能是有副作用的导入被误删
'no-undef': 'error', // 阻断:运行时必炸
quotes: ['warn', 'single'], // 提示:Prettier 的事,不该占用人注意力
semi: ['warn', 'always'],
'max-len': 'off', // 直接关掉,交给格式化工具
},
};
自动 Fix 最典型的坑:import './polyfill' 被判定为未使用变量删掉,构建产物在低版本浏览器直接白屏。格式问题交给工具,人的注意力留给工具看不见的东西。
L2 语法与业务逻辑:三年经验也常踩空
这一层考代码本身的正确性,检查点密集:
- 类型是否完备,有没有漏掉边界判断
- 参数校验:入参为空、undefined、null 场景是否都兜住
- 重复代码识别:复制粘贴出来的业务逻辑,该不该抽公共工具函数
- 副作用问题:函数是否纯函数,有没有随意修改入参对象
- 魔法数字与常量:
if (status === 3)这种散落各处的硬编码
入参修改是最常见的翻车点:
ts
// bad:改了入参对象,调用方数据被污染,还漏了兜底
function normalizeUser(user: User) {
user.name = user.name.trim();
if (user.age > 18) user.adult = true;
return user;
}
// good:不改入参,返回新对象,边界先兜住
function normalizeUser(input: UserInput): User {
const name = input?.name?.trim() ?? '';
const age = Number.isFinite(input?.age) ? input.age : 0;
return { ...input, name, age, adult: age >= 18 };
}
注释也得有判断力。必须写的是"为什么这么做"(踩过的坑、绕开的兼容性),不是"做了什么"。// 把 count 加一 这种复述代码的注释,删掉比留着强;注释掉的死代码更是直接删。
L3 异常与副作用:这里才见真功夫
到这一层,评审的是"这段代码上线会不会出事"。
接口请求有没有补异常,失败之后页面如何降级?权限埋点逻辑会不会漏写多写?if-else 嵌套超过四层、条件分支漏了 default,都是典型漏网之鱼。
状态更新风险。组件状态修改造成死循环渲染,是前端特有的一类 bug:
副作用清理同理------定时器、事件监听、请求,组件销毁时统统要释放,否则就是内存泄漏。
tsx
// bad:请求失败无兜底,定时器不清理,卸载后照样 setState
useEffect(() => {
fetch('/api/list').then(r => r.json()).then(setList);
const timer = setInterval(poll, 3000);
}, []);
// good:中断 + 存活标记 + 清理函数
useEffect(() => {
const ac = new AbortController();
let alive = true;
fetch('/api/list', { signal: ac.signal })
.then(r => { if (!r.ok) throw new Error(`HTTP $${r.status}`); return r.json(); })
.then(d => { if (alive) setList(d); })
.catch(e => { if (alive) setError(e.message); });
const timer = setInterval(poll, 3000);
return () => { alive = false; ac.abort(); clearInterval(timer); };
}, []);
能在这个层面拦下问题的评审者,价值远超"改格式"。
L4 架构与可维护性:高级开发的分水岭
- 模块职责划分:单个文件、单个函数是不是职责过重
- 依赖引入:有没有引入冗余包,有没有循环依赖
- 组件设计:传参是否合理,Props 有没有做约束,是否存在过度封装
- 复用逻辑:hooks 和工具函数设计得够不够通用,有没有写一堆一次性临时逻辑
- 兼容性:新 API 有没有降级方案,目标浏览器环境评估过没有
Props 裸奔是最典型的:
tsx
// bad:传错类型运行时才炸
function Chart({ data, type, config }) { /* ... */ }
// good:联合类型把非法值挡在编译期
type ChartProps = {
data: number[];
type: 'line' | 'bar';
config?: Partial<ChartConfig>;
};
function Chart({ data, type, config = {} }: ChartProps) { /* ... */ }
L5 性能与安全:事故防线
最后一层,触及线上事故底线。
大数据列表一次性渲染卡死页面、不必要的重渲染、死循环------性能问题这里过一遍。
XSS 更直接:用户输入直接塞进 innerHTML,等于把站点交给攻击者。
ts
// bad:直接渲染用户输入
el.innerHTML = `<div>$${comment}</div>`;
// good:要么用 textContent,要么必须过滤
el.textContent = comment;
// 富文本场景
el.innerHTML = DOMPurify.sanitize(comment, { ALLOWED_TAGS: ['b', 'i', 'a'] });
还有两件常被忽略的事:密钥有没有写死在前端代码里(打包后的 dist 目录一 grep 就出来);上线出问题,这段改动能不能快速回滚------一个 PR 混进五个不相关的功能,revert 就是灾难。
新增逻辑有没有补对应的单元测试用例,也在这层问。这个才叫专业。
缺陷优先级:说不出来就是没判断力
说不出优先级,等于没有判断力。我的分法:
| 级别 | 判定标准 | 处理方式 |
|---|---|---|
| P0 阻断 | XSS、内存泄漏、死循环、密钥硬编码 | 必须改完再合 |
| P1 严重 | 逻辑漏洞、异常未处理导致功能不可用 | 本次迭代内修复 |
| P2 一般 | 重复代码、职责过重、命名有误导性 | 记录 issue,可先合 |
| P3 建议 | 格式、注释风格、个人偏好 | 交给 ESLint 自动修复 |
关键不是分级本身,而是你知道哪一类问题一旦漏掉,代价是凌晨三点的线上事故。
写在最后
同样一份 CR,有人只看格式排版,有人能提前拦住线上事故。这就是技术深度。
我反问候选人:你现在觉得 CodeReview 真的就只是改改代码格式吗?
你们做 CodeReview 踩过最坑的问题是什么?是某个被放过去的 useEffect,还是一次让整个模块回滚的误合并?评论区聊聊。
有用的话点个赞。