前端代码质量 —— 可读性

0 阅读6分钟

Snipaste_2026-03-30_18-49-48.png

可读性

代码是否容易看懂、容易顺着读下去、容易快速确认主流程。

命名

业务含义应被显式表达,避免依赖上下文猜测、个人习惯或隐含约定。

魔法值

是否存在缺少业务语义的数值或字符串。

  • 魔法数值:直接写入代码但缺少解释的数字
  • 魔法字符串:直接写入代码但缺少业务语义的字符串
  • 建议:优先通过常量、枚举、配置项等方式具名表达

命名

命名是否清晰、准确、一致。

常量
  • 谨慎使用下划线:前端业务代码中通常避免使用下划线 _,下划线通常用于常量中的语义分隔,如 MAX_VALUE
  • 常量命名:普通常量建议使用全大写加下划线,即 UPPER_SNAKE_CASE
变量名

看名字能否看出来源、用途、状态。

  • 前端最容易起空的词datalistiteminfovalueobjarrtempflagstatustyperesultprocessmanagerutilcommoncurrentnextactive
  • 什么时候这些词可以用:作用域较小、上下文已足够明确时可以接受;一旦变量跨函数、跨模块或承载明确业务语义,应优先使用更具体的业务命名
函数名

看名字能否看出动作和对象。

  • 常用动作前缀
    • 提交:submitsavesendcreate
    • 获取:getfetchloadquery
    • 更新:updatesetresetpatch
    • 删除:removedeleteclear
    • 校验:validatecheck
    • 判断:ishascanshould
    • 转换:buildmaptransformnormalizeformatparseserialize
    • 交互:handleopenclosetoggleshowhideselect
    • 同步:syncpersisttrackemitnotify
  • 什么时候可以省略对象:在表单、表格、弹窗、鼠标交互等事件语境很强的场景中,handleSubmithandleClickhandleChange 这类命名通常可以接受;若函数承担明确业务动作,仍应优先补充业务对象
组件名

看名字能否看出职责和交互形态。

  • 常见交互形态PageListTableFormModalDialogDrawerPanelCardTagBadgeSelectorPickerDetailTabsToolbarFilterMenuItemRowEmptyStateSkeleton
  • 组件命名建议:组件名通常使用名词或名词短语,推荐优先体现业务对象和交互形态,如 UserTableOrderFormFilterPanel
布尔值

看名字是否像一句可判断真假的话。

  • 普通布尔变量:常用 ishascanshould
  • 可见性、进行中的状态:如 isVisibleisRunning
  • 属性或 props:可直接使用状态词,如 disabledselectablehoveredclickable
  • 其他常见表达:组件附加能力如 withTab;启用能力如 enableFilter;允许或限制如 allowXXXnoXXX

逻辑复杂度

本质上通常是职责膨胀:一个函数或组件承担了过多判断、状态、分支和依赖,导致主流程不清晰、阅读路径变长、修改风险升高。

  • 核心判断:是否违反单一职责原则
  • 常见表现:分支过多、嵌套过深、职责混杂、重复判断、状态组合过多
  • 直接结果:代码未必很长,但主流程难以一眼读懂,改一个条件容易影响别的路径

圈复杂度

圈复杂度(Cyclomatic Complexity)是衡量判定结构复杂度的指标,本质上反映代码中独立执行路径的数量。

  • 可理解方式
    • 独立执行路径越多,理解成本越高
    • 覆盖所有可能情况所需的最少测试用例通常也越多
  • 常见来源
    • if / else if / else
    • switch / case
    • 三元表达式
    • 逻辑短路分支
    • 循环中的条件判断
  • 需要注意:圈复杂度只是“路径数量”的度量,不完全等于可读性;有些代码圈复杂度变化不大,但认知复杂度和阅读成本已经明显下降

需要重点识别的问题

分支过多

条件路径是否过多,主流程是否被拆碎。

  • 典型表现
    • 连续 if / else if / else
    • 大段 switch / case
    • 同一业务条件在不同位置反复判断
    • 多个布尔值交叉组合决定不同行为
  • 风险
    • 分支一多,遗漏路径、冲突路径、无效路径的概率会明显上升
    • 测试覆盖难度同步上升
嵌套过深

层级是否过深,理解路径是否过长。

  • 典型表现
    • ifif
    • 回调里再包回调
    • 组件里套很多仅为转发逻辑的中间组件
    • 一个 Hook 内部串很多 Hook 和派生逻辑
  • 风险
    • 阅读者需要同时记住更多上下文
    • 主流程被包裹在多层条件里,不容易快速定位
职责混杂

一个函数或组件是否同时承担了过多不同责任。

  • 典型表现
    • 既做数据转换,又做权限判断,还做 UI 渲染
    • 既决定业务状态,又顺手发请求、打点、弹 toast
    • 同时处理正常流程、异常流程、边界兜底、埋点和缓存
  • 风险
    • 逻辑膨胀后,状态会越来越多,分支会进一步放大
    • 小改动也容易牵动无关逻辑

常见优化手法

提前返回

提前返回不一定降低圈复杂度,但通常能降低嵌套深度,让主流程更清晰。

  • 价值
    • 先处理异常路径、边界路径
    • 把主流程留在最外层
    • 减少一层层 else
  • 核心收益:不一定减少“路径数”,但能明显改善“阅读顺序”
用策略表替代 if / switch

把“分支选择”改成“按 key 查表”,把流程分叉转成配置分发。

if (status === 'idle') { ... }
else if (status === 'loading') { ... }
else if (status === 'error') { ... }

const handlerMap = {
  idle: handleIdle,
  loading: handleLoading,
  error: handleError
}

handlerMap[status]?.()
  • 适用场景
    • 状态到行为的一一映射
    • 类型到渲染器的映射
    • 平级分支很多,但结构相似
  • 注意
    • 只有在“分支本质是查找映射”时才适合
    • 如果每个分支本身逻辑差异极大,硬塞策略表反而可能增加跳转成本
合并条件 / 去重复判断

同一个前置条件不要在多个位置重复判断,能合并就合并,能收敛就收敛。

if (a) {
  doSomething()
}

if (a && b) {
  doAnother()
}
if (a) {
  doSomething()
  if (b) {
    doAnother()
  }
}
  • 收益
    • 减少重复阅读同一条件
    • 让条件归属关系更清晰
    • 降低后续修改条件时漏改的概率
函数拆分

不是为了拆而拆,而是把复杂判断、复杂规则、复杂分支“命名”掉,让主流程留在当前层。

  • 好的拆分
    • isRetryAllowed(record)
    • resolveDisplayStatus(record)
    • buildExportPayload(config)
  • 价值
    • 主流程复杂度下降
    • 复杂判断被具名后,语义更清晰
    • 细节可以被局部封装,但主流程仍然容易读
  • 注意
    • 不要拆成大量只有一两行、却没有新语义的小函数
    • 如果拆分后必须来回跳多个文件才能看懂,反而会增加理解成本

评审时可追问的问题

  • “主流程能否一眼看出来?”
  • “这里的复杂度来自必要业务,还是职责混杂?”
  • “这个分支是否能前置 return、合并判断或改为查表?”
  • “这个拆分是在降低复杂度,还是在转移复杂度?”
  • “修改其中一个条件时,会不会影响到别的隐含路径?”