阅读视图

发现新文章,点击刷新页面。

前端空值处理规范:Vue 实战避坑,可选链、?? 兜底写法|项目规范篇

帮助同学们学会在前端真实业务项目里,到底该怎么写空值处理(?.、??、||、if判断、兜底逻辑),以及为什么这么选、会踩哪些高频坑,顺便帮你拉直JS/TS空值、真值假值的基础概念,助力写出规范可维护的团队级代码。

在这里插入图片描述

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。

引子:为什么要专门聊“空值处理规范”?

一句话定位这篇文章:

教你在真实项目里,到底该怎么写空值处理( ?. ?? || if ** 判断、兜底逻辑),以及为什么这么选、会踩哪些坑**,顺便帮你把 JS/TS 的一些基础概念拉直。

适用人群:

  • 已经会写 JS / Vue,但概念有点混== null||?.?? 到底差在哪?

  • 刚入门前端的小伙伴:想从一开始就养成靠谱的代码习惯

  • 像我这样工作多年想回炉重造的工程师:系统校准一下“老习惯”是不是已经过时了 本文不会讲太多过度底层的规范条文,而是:

  • 围绕真实业务代码的写法

  • 配合 完整示例 + 场景解释

  • 重点放在:怎么选写法、为什么这么选、常见坑在哪里

一、先把“空值家族”讲清楚:null、undefined、空字符串、0、false…

日常开发中经常混在一起的几个值:


null           // 明确的“空值”,一般表示“这里有个位置,但现在没有值”
undefined      // 未定义,通常是“压根没传”、“没赋值”
''             // 空字符串
0              // 数字 0
false          // 布尔 false
NaN            // 不是一个合法数字

1.1 “真值/假值”概念(很关键)

在 JS 里,if (xxx) 判断的是“真值/假值(truthy / falsy)”,而不是严格意义上的 true/false。下面这些都是 falsy(假)

  • false
  • 0
  • -0
  • ''(空字符串)
  • null
  • undefined
  • NaN

其他的基本都被当成 truthy(真)

为什么要先讲这个?

因为 ||&& 这些逻辑运算符,走的就是“真值/假值”逻辑。

比如:


const value = 0;
const result = value || 100;
console.log(result); // 100,而不是 0

0 在 JS 里是假值,所以 value || 100 会拿到 100

这也是我们后面会反复提的一个大坑:“用 ** || ** 做默认值会把合法值 0/''/false 当成没传”

二、可选链 ?.:安全访问深层属性的标准写法

场景:从后端拿到一个复杂对象,但某一层可能是 null / undefined,直接访问就会炸:


// 假设 user 可能是 null
const city = user.profile.address.city; 
// TypeError: Cannot read properties of null (reading 'profile')

2.1 传统写法 VS 可选链

传统写法(防御式编程):


const city =
  user &&
  user.profile &&
  user.profile.address &&
  user.profile.address.city;

  • 可读性差
  • 很啰嗦
  • 稍微一改结构就容易漏一个判断

可选链写法:


const city = user?.profile?.address?.city;

  • 短很多
  • 语义清晰:如果中间任何一层是 null/undefined,就直接返回 undefined,而不是抛异常

2.2 在 Vue 模板里的使用

Vue 2 + Babel 环境Vue 3 默认 Vite 脚手架 一般都支持可选链。

在模板里:


<template>
  <div>
    <p>用户名:{{ user?.profile?.name || '未设置' }}</p>
    <p>城市:{{ user?.profile?.address?.city || '未知城市' }}</p>
  </div>
</template>

<script setup>
const user = ref(null);
// 后端请求完成后,再赋值
</script>

注意:模板表达式里也可以用 ?.||??,和 JS 里一样。

2.3 规范建议:何时必须用可选链?

我在项目里通常建议:

  • 从接口拿来的数据 + 多层嵌套对象默认用可选链
  • SDK / 第三方库返回的结构:尽量用可选链保护
  • 对于我们自己完全可控、结构固定的内部数据,可以不用(比如本地写死的配置)

统一规则示例:

  • 接口 Model 层(TypeScript 类型 + 接口封装):尽量把可选属性处理掉,往下传固定结构
  • 页面 / 组件层
    • 对于“接口原始数据”:用 ?. + 兜底字符串 / 兜底组件
    • 对于“内部状态”:减少可选,用默认值初始化

三、空值合并运算符 ??:给“真空”兜底,而不是给所有假值兜底

回顾刚才的例子:


const value = 0;
const result = value || 100;
console.log(result); // 100

如果 0 在业务里是合法值(比如“价格 0 元”、“数量 0 个”),那上面这行其实是错的。

我们想要的是:“只有在值为 null 或 undefined 的时候才给默认值”。

这就是 ?? 的作用。

3.1 || vs ?? 对比示例


console.log(0 || 100);       // 100
console.log(0 ?? 100);       // 0

console.log('' || '默认');   // '默认'
console.log('' ?? '默认');   // ''

console.log(null || '默认'); // '默认'
console.log(null ?? '默认'); // '默认'

console.log(undefined || '默认'); // '默认'
console.log(undefined ?? '默认'); // '默认'

总结一句话:

  • ||:只要左边是假值(包括 0 / '' / false / NaN / null / undefined),就用右边
  • ??:只有左边是 nullundefined 时,才用右边

3.2 在真实业务中的推荐用法

典型错误写法(很常见):


// 单价和数量来自接口
const price = item.price || 0;
const count = item.count || 1;
const total = price * count;

在这些场景会出错:

  • 价格为 0 元:price 会变成 0 || 0 → 0(这里还好)
  • 数量为 0:count 会变成 1(业务错了)
  • 用户输入了空字符串 '' 需要区分,但被直接当成没填

推荐写法:


const price = item.price ?? 0;  // 价格缺失才用 0
const count = item.count ?? 1;  // 只有未传 count 才默认 1

再比如配置项对象


function createDialog(options = {}) {
  const width = options.width ?? 400;         // 未传 width 才采用默认 400
  const closable = options.closable ?? true;  // 未传 closable 才用 true
}

3.3 在 Vue 模板中用 ??


<template>
  <div>
    <!-- 后端没给 nickName 时显示 '游客',但如果是空字符串就保持空 -->
    <p>昵称:{{ user.nickName ?? '游客' }}</p>
  </div>
</template>

规范建议:

  • 只要你的兜底逻辑只想针对 null/undefined,统一用 ??,不要用 ||
  • 保留 || 用于“逻辑或”场景,而不是“兜底默认值”。

四、兜底逻辑:不仅是运算符,还有“业务上的安全网”

可选链和空值合并属于“语法层面的防御”。

真实项目里,还需要“业务层面的兜底”,比如:

  • 数据为 null 时显示一个“空态组件”
  • 钱包余额为 null 时,不显示数字而是展示“--”
  • 列表为空时展示“暂无数据”

4.1 文本兜底:别让页面渲染出 undefined / null

错误示例:


<template>
  <div>
    <!-- 假设 user.name 可能 undefined -->
    <p>用户名:{{ user.name }}</p>
  </div>
</template>

页面可能出现:


<p>用户名:undefined</p>

推荐写法:


<template>
  <div>
    <p>用户名:{{ user?.name ?? '未设置' }}</p>
  </div>
</template>

如果你更谨慎一点,还可以抽成一个小工具函数或指令:


function displayText(value, fallback = '--') {
  if (value === null || value === undefined) return fallback;
  return String(value);
}

模板中:


<p>用户名:{{ displayText(user?.name, '未设置') }}</p>

4.2 数字兜底:0、null、undefined 要区分

常见场景:金额 / 数量 / 积分


<template>
  <div>
    <!-- 如果 amount 为 0,要显示 0 元,而不是 “--” -->
    <p>金额:{{ formatAmount(order?.amount) }}</p>
  </div>
</template>

<script setup>
function formatAmount(value) {
  if (value === null || value === undefined) return '--'; // 真空
  const num = Number(value);
  if (Number.isNaN(num)) return '--';                     // 非法数字
  return num.toFixed(2) + ' 元';
}
</script>

这里的思路是:

  • 对于“真空”(null/undefined)和“非法值”(NaN),直接兜底成 --
  • 对于合法的 0、10.5 等,按正常格式化逻辑展示

4.3 列表兜底:空数组 vs null/undefined

错误写法:


<template>
  <ul>
    <li v-for="item in list" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

<script setup>
const list = ref(null);
</script>

list 为 null 时,Vue 其实不会崩溃,但可读性很差,而且 TypeScript 下会疯狂报错。

推荐规范:

  • 列表类型的数据,初始化为 [],不要初始化为 null

  • 接口响应里如果是 null在数据层统一转成 [],不要把“既可以是数组又可以是 null”的结构传到视图层


// 假设后端可能返回 { list: null }
interface ApiResponse<T> {
  list: T[] | null;
}

async function fetchUsers(): Promise<User[]> {
  const res: ApiResponse<User> = await request('/api/users');
  return res.list ?? [];
}

Vue 组件里直接:


const users = ref<User[]>([]);

onMounted(async () => {
  users.value = await fetchUsers(); // 一定是数组
});

好处:

  • 模板里 v-for="user in users" 不用可选判断
  • 业务逻辑中也不用 if (!users) 乱判
  • 类型更干净,TS 也容易推断

五、可读性 vs 防御性:别让“防空代码”毁了代码结构

经常看到这样的代码:


if (user && user.profile && user.profile.address && user.profile.address.city) {
  showCity(user.profile.address.city);
} else {
  showDefaultCity();
}

可读性非常差。我们可以结合 ?. 和业务逻辑重写:

5.1 利用中间变量提高可读性


const city = user?.profile?.address?.city;

if (city) {
  showCity(city);
} else {
  showDefaultCity();
}

如果业务含义更复杂,比如:

  • city 为空字符串也视为没填

可以:


const rawCity = user?.profile?.address?.city;
const city = rawCity?.trim(); // string 或 undefined

if (!city) {
  showDefaultCity();
} else {
  showCity(city);
}

规范建议:

  • 不要在 if (...) 里面写一大串可选链,可以先提取出来
  • 对于复杂逻辑(例如 if (a && b && c && d)),考虑拆成几个语义明确的变量

六、项目中推荐的“空值处理规范(示例版)”

以下是一份可直接落地到团队规范里的示例,你可以根据团队实际情况调整。

6.1 基础规则

  • 规则 1:接口层统一做“空值归一化”
    • 列表字段:null / undefined 统一转成 []
    • 数字字段:null / undefined 转成约定好的业务默认(如 0),或者保持 null,但要有清晰设计文档
    • 字符串字段:如果是必展示项,可以转 '',或保留 null,但组件层要有兜底文案
  • 规则 2:组件 / 页面层永远不要直接信任后端
    • 访问深层属性一律用 ?.
    • 模板输出中不要让 null / undefined 直接裸露
  • 规则 3:兜底默认值尽量用 ??,而不是 ||
    • 只有当你有意要把 0 / '' / false 也视为“空”时,才可以用 ||

6.2 风格对比示例(推荐 vs 不推荐)

不推荐:


// 1. 访问深层属性不做保护
const city = user.profile.address.city;

// 2. 用 || 做默认值
const price = item.price || 0;
const count = item.count || 1;

// 3. 列表用 null 表示“还没加载”
const list = ref(null);

推荐:


// 1. 使用可选链保护
const city = user?.profile?.address?.city;

// 2. 用 ?? 严格处理 null/undefined
const price = item.price ?? 0;
const count = item.count ?? 1;

// 3. 列表统一用 [] 作为初始值
const list = ref([]);

在 Vue 模板中的统一写法示例:


<template>
  <div>
    <p>用户名:{{ user?.name ?? '未设置' }}</p>
    <p>年龄:{{ user?.age ?? '--' }}</p>

    <p>余额:{{ formatAmount(account?.balance) }}</p>

    <ul v-if="orders.length">
      <li v-for="order in orders" :key="order.id">
        订单号:{{ order.id }},金额:{{ formatAmount(order.amount) }}
      </li>
    </ul>
    <p v-else>暂无订单</p>
  </div>
</template>

<script setup>
const user = ref(null);
const account = ref(null);
const orders = ref([]); // 一定是数组

function formatAmount(value) {
  if (value === null || value === undefined) return '--';
  const num = Number(value);
  if (Number.isNaN(num)) return '--';
  return num.toFixed(2) + ' 元';
}
</script>

七、常见踩坑案例拆解

7.1 “把 0 当成没填”——报表类页面的大坑

需求:展示一个指标的环比增长率,后端字段 growthRate,可能是:

  • 0:说明没涨没跌
  • 正数:增长
  • 负数:下降
  • null:没有数据

错误写法:


<p>环比:{{ growthRate || '--' }}%</p>

growthRate = 0 时,会显示 --%,业务含义严重错误。

正确写法:


<p>环比:{{ growthRate ?? '--' }}{{ growthRate === null || growthRate === undefined ? '' : '%' }}</p>

或者包装一下:


function displayPercent(value) {
  if (value === null || value === undefined) return '--';
  return `${value}%`;
}

模板:


<p>环比:{{ displayPercent(growthRate) }}</p>

7.2 “深层属性访问炸页面”——常见于接口变更

场景:后端有一天把 user.profile 改成 user.info,但你代码里到处是:


user.profile.address.city

迁移时推荐策略:

  1. 先统一加可选链防御(短期止血):

const city = user?.profile?.address?.city;

  1. 在“数据适配层”做映射,避免在视图层直接跟后端结构硬绑定:

interface UserViewModel {
  city?: string;
  // ...
}

function mapUserDtoToViewModel(dto: any): UserViewModel {
  const profile = dto.profile || dto.info || {};
  return {
    city: profile.address?.city,
    // ...
  };
}
  1. 视图层只用 viewModel.city,再配合兜底:

<p>城市:{{ user.city ?? '未知城市' }}</p>

这样即使后端再改结构,你只需要改映射函数,不会到处是 ?. 打补丁。

八、结合 TypeScript:从“到处防空”升级为“类型上减少空值”

如果你的项目已经用 TypeScript,可以进一步 把“空值问题”提前到类型设计阶段解决

8.1 接口类型:把“可选”缩到最小

错误示例(很多后端生成工具会这样):


interface UserDto {
  id?: number;
  name?: string;
  age?: number | null;
  address?: {
    city?: string;
  } | null;
}

视图层到处是:


user?.address?.city ?? '未知城市'

更好的做法是:

  • 在“接口模型”层承认这些都是可选

  • 但在往页面传的时候,通过构造 ViewModel 把这些变成“非可选 + 有默认值”


interface UserViewModel {
  id: number;
  name: string;
  age: number | null;   // 业务上允许为 null
  city: string;         // 至少有兜底
}

function toUserViewModel(dto: UserDto): UserViewModel {
  return {
    id: dto.id ?? 0,                         // 或抛错,看业务
    name: dto.name ?? '未命名用户',
    age: dto.age ?? null,
    city: dto.address?.city ?? '未知城市',
  };
}

组件里就可以大胆用:


<p>用户名:{{ user.name }}</p>
<p>城市:{{ user.city }}</p>

而不是到处防空。

九、落地建议:如何在现有项目里逐步推行这套规范?

9.1 从“新代码”开始做对

  • 自己写的新组件、新方法,从一开始就用 ?.??
  • 审 PR 的时候,对用 || 做默认值的地方特别敏感,看清楚是否需要保留 0/''/false

9.2 为高风险页面补一层“空值巡检”

优先排查:

  • 面向 C 端用户的关键页面(订单、支付、结算)
  • 报表、数据面板类页面(数字特别多)

从这些点切入:

  • 所有深层属性访问,加上可选链或前置的空值判断
  • 所有数值展示,考虑是否需要 formatXXX 方法来统一兜底逻辑
  • 所有默认值逻辑,检查 || 能否替换为 ??

9.3 写到团队规范 / README / Contributing 里

可以直接摘抄下面一段到你们项目的规范文档里:

空值处理规范(摘要)

  1. 从接口拿到的原始数据,访问深层属性一律使用可选链 ?.
  2. 兜底默认值优先使用空值合并运算符 ??,只有在需要把 0 / '' / false 也当成“空”的场景才使用 ||
  3. 列表数据初始化为 [],不要用 null 表示“尚未加载”。接口返回 null 时在数据适配层统一转为 []
  4. 数字和金额展示需通过统一的格式化方法处理,避免页面出现 NaNundefined
  5. 模板中禁止直接输出可能为 null / undefined 的字段,必须有兜底显示(如 '--''未设置' 等)。

十、总结:把“空值处理”当成一个硬规范,而不是临时脑补

  • 可选链 ?.:用来安全访问深层属性,防止“Cannot read properties of undefined” 直接把页面干崩。
  • 空值合并 ??:只在 null / undefined 时兜底,避免误伤合法的 0 / '' / false
  • 兜底逻辑:不仅是语法问题,更是业务体验和数据安全网的问题,最好沉淀为项目级规范,而不是随手一写。

技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

前端代码注释规范:Vue 实战避坑,让 3 年后的自己还能看懂代码|项目规范篇

一套真正能落地的前端代码注释规范,从 Vue 项目实战出发,告诉你注释该写什么、不该写什么,避开常见坑点,写出让 3 年后的自己还能看懂的可维护代码。

在这里插入图片描述

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。

前言:为什么要认真对待“写注释”这件小事?

你有没有遇到过这些场景:

  • 半年前自己写的业务,今天改个小需求,打开文件之后第一反应:“这谁写的垃圾代码?”,再一看作者:是自己。

  • 接手别人老项目,逻辑绕来绕去,偶尔看到一行注释:// TODO// 这里有点问题,先这么写……然后就没有然后了。

  • 为了“规范”,团队强行要求每个函数、每个变量都加注释,结果:注释和代码一起过期,甚至误导后来的人。

这篇文章就想解决一个现实问题:

日常写代码时,注释到底该怎么写?为什么这么写?坑会踩在哪?

目标是:让 3 年后的自己和队友,打开代码就能快速搞懂上下文,而不是骂人。

本文不是讲晦涩的底层原理,而是站在一线开发、项目规范的视角,用 Vue / 前端开发场景来聊聊“代码注释规范”。

一、第一原则:好代码胜过好注释,但没有注释也不一定是好代码

1.1 一句话核心原则

能用清晰的命名和结构表达含义,就不要用注释补课。注释只做代码无法表达的“额外信息”。

很多团队会陷入两个极端:

  • 极端 1:注释洁癖“好的代码不需要注释”,结果写一堆晦涩难懂的缩写变量,没人看得懂。

  • 极端 2:注释狂魔几乎每一行都要注释:

    
    // 声明一个变量 a
    let a = 1;
    // a 加 1
    a++;
    
    

    这种注释只会浪费时间、增加维护成本。

正确姿势:

  • 优先改代码,让代码本身更清晰(变量名、函数名、拆分方法、抽象组件……)

  • 其次用注释补充“代码表达不到的信息”,例如:

    • 为什么要这么写(业务背景 / 历史原因 / 兼容性)
    • 注意事项(性能、边界条件、已知坑)
    • 和其他模块的约定(接口协议、调用顺序)

二、注释的四大黄金场景:该写什么?

下面是我在项目里常用、非常推荐的四类注释场景。

2.1 解释“为什么这么写”(Why),而不是“代码在干嘛”(What)

What 代码自己能看出来,Why 只能靠你写出来。

❌ 错误示例:只是重复代码


// 获取用户列表
const users = await fetchUsers();

  • 这行注释几乎就是在重复变量名,没有信息增量

✅ 推荐示例:解释设计/业务原因


// 这里不能直接用缓存的用户列表:
// 1. 用户状态(在线/离线)是实时的
// 2. 后端会根据当前登录态过滤可见用户
// 所以每次都强制请求最新数据
const users = await fetchUsers({ forceRefresh: true });

这里的注释说明了为什么不能优化成缓存,以后有人想“优化性能”时,看到注释就会收手,避免踩坑。

2.2 标记“约定”和“前置条件”:别人需要遵守什么?

在 Vue 组件、工具函数、API 调用中,最容易出问题的往往不是“实现细节”,而是使用前提

  • 参数有没有默认值?
  • 有哪些边界情况?
  • 调用顺序有没有依赖?

✅ Vue 组件示例:在 props / emits 上写注释


// UserForm.vue <script setup lang="ts">
interface Props {
  /**
   * 表单模式:
   * - 'create':新建用户,所有字段可编辑
   * - 'edit':编辑用户,用户名不可修改
   * - 'readonly':只读模式,所有字段禁用
   */
  mode: 'create' | 'edit' | 'readonly';

  /**
   * 编辑/只读模式下必传:
   * 后端返回的完整用户信息。
   * create 模式下可以不传(内部会使用默认值)
   */
  user?: User;
}

const props = defineProps<Props>();

/**
 * 表单提交事件:
 * - create: 提交的 user.id 由后端生成
 * - edit: 必须包含原有的 user.id
 */
const emit = defineEmits<{
  (e: 'submit', payload: User): void;
}>();

这里注释的作用非常明确:

  • 告诉你 mode 不同模式的差别
  • 告诉你 user 在什么模式下是必传的
  • 告诉你 submit 的 payload 长什么样

重点:这类注释是“契约”的一部分,写在类型(interface / props / emits)附近最合适。

2.3 记录“历史遗留”和“坑点说明”:这块代码为什么这么丑?

有些代码你也知道写得不优雅,但短期内又不能重构,比如:

  • 老接口的奇怪字段命名
  • 历史版本遗留的时间格式
  • 奇怪的兼容写法(低版本浏览器 / 特定设备)

与其未来被队友(或自己)怒喷:

“这谁写的?怎么这么鬼畜?”

不如提前写清楚原因。

✅ 示例:兼容老接口


/**
 * 注意:后端这个接口是老系统保留的,字段命名非常诡异。
 * - 'usr_nm' 对应用户姓名
 * - 'crt_tm' 是创建时间字符串,格式为 'YYYY/MM/DD HH:mm:ss'
 * 暂时不能动这个接口,只在这里统一做一次映射。
 */
function normalizeLegacyUser(raw: any): User {
  return {
    id: raw.id,
    name: raw.usr_nm,
    createdAt: dayjs(raw.crt_tm, 'YYYY/MM/DD HH:mm:ss').toDate(),
  };
}

以后谁要改这个接口时,看到注释就会明白:

  • 这是历史债务,不是你写代码水。
  • 如果要改,要 连后端 / 老系统一并考虑

2.4 对复杂算法 / 业务流程做“概览说明”:给后人一张思维导图

有些模块就算代码写得再优雅,逻辑本身就是复杂的

  • 多步骤审批流
  • 复杂的优惠券 / 价格计算规则
  • 权限控制(菜单 + 按钮 + 数据权限)

这种时候,不要指望“代码自解释”,加一段流程性注释是对所有人的救赎。

✅ 示例:订单价格计算(假设你在 calculateOrderPrice.ts 里)


/**
 * 订单价格计算规则(简化版):
 *
 * 1. 基础金额 = 所有商品单价 * 数量 之和
 * 2. 商品级优惠:
 *    - 满减券:优先按商品分类应用,不能跨分类凑单
 *    - 折扣券:在满减之后应用,最多 2 张
 * 3. 订单级优惠:
 *    - 平台券:在所有商品级优惠之后应用
 *    - 封顶逻辑:总优惠金额不能超过基础金额的 30%
 * 4. 运费:
 *    - 满 99 元包邮
 *    - 其他情况按地区和重量计算
 *
 * 注意:
 * - 所有金额都用「分」为单位在内部计算,避免浮点误差
 * - 对外展示时再转换为「元」
 */
export function calculateOrderPrice(order: Order): OrderPriceDetail {
  // 具体实现略
}

这里注释的价值在于:

  • 给出了整体流程(按步骤)
  • 标明了关键约束(封顶 30%、单位是“分”)
  • 以后别人改逻辑时,有一个可以“对齐口径”的地方

三、哪些注释是坚决不要写的?

知道“该写什么”之后,更重要的是:哪些注释写了只会拖团队后腿?

3.1 重复代码的注释:浪费时间 + 增加维护成本

❌ 示例 1:重复变量名


// 用户名称
const userName = getUserName();

❌ 示例 2:重复函数名 / 类型名


/**
 * 获取用户列表
 */
function getUserList() { ... }

这些注释的问题:

  • 没有额外信息
  • 只要一改函数名/变量名,注释就有可能不一致
  • 时间久了变成“看着像对的,其实是错的”

解决办法:

  • 优先把命名改清晰:getListgetUserListdatauserList / formState
  • 确实没啥要补充的,就不要写注释,空着反而更安全。

3.2 “心情日志”注释:TODO / FIXME 不写清楚内容

❌ 典型反面教材:


// TODO: 后续优化
// FIXME: 有 bug

半年后你自己也不知道:

  • 要优化什么?
  • 有什么 bug?复现步骤是什么?
  • 是否已经修了?是否还有影响?

✅ 推荐写法:


// TODO(v2.1): 表格数据量>1w时,滚动卡顿,需要引入虚拟列表
// 影响范围:订单列表、用户列表


// FIXME(2025-03-18 by 张三):
// 后端偶发返回重复的 orderId,导致 set 里丢数据
// 临时方案:前端用 (orderId + createdAt) 拼接作为 key,等后端修复后移除

规范建议:

  • TODO / FIXME 注释建议包含:

    • 触发条件 / 复现方式
    • 影响范围
    • (可选)目标版本/时间 & 责任人缩写
  • 团队可以规定:重要 TODO / FIXME 必须对应 Jira/禅道/飞书任务号,比如:


// TODO(JIRA-1234 v2.2): 支持多语言,先写死为中文

3.3 和真实逻辑不一致的注释:比没有注释更可怕

注释一旦和代码不一致,就会变成误导信息

❌ 示例:注释没更新


/**
 * 返回 true 表示用户未登录
 */
function isLoggedIn() {
  return !!localStorage.getItem('token');
}

显然逻辑是“有 token 才是登录”,但注释写反了。

如果后来别人只看注释不看实现,很容易写出一堆反逻辑的代码。

经验结论:

写过时注释 = 欺骗未来的同事。

写了就要维护,维护不了就少写。

所以在团队规范里可以明确:

  • 改动逻辑时,必须同步检查相关注释是否仍然正确
  • Code Review 时,把**“注释是否仍然成立”**当成一个检查点

3.4 写在实现细节里的“小说故事”:越写越乱

有同学特别喜欢在函数内部“边写边感想”,比如:


function fetchData() {
  // 这里先判断一下是不是有缓存
  // 如果有缓存的话就不用请求接口了
  // 但是这里我们又觉得可能缓存会不准
  // 所以又加了一个时间戳的判断
  // 总之就是很复杂,先这么写吧……
}

这种注释的问题:

  • 没有结构,像碎碎念日记
  • 讲了一堆感受,没有讲清楚最终规则
  • 以后别人看的时候,只会更迷惑

更好的做法:

  • 把真正关键的规则整理成条目
  • 其他的犹豫、不确定、吐槽,写到需求文档 / 评审记录里,而不是代码里

✅ 重写示例:


/**
 * 缓存策略说明:
 * 1. 默认命中缓存,避免重复请求
 * 2. 如果缓存时间超过 5 分钟,则强制请求最新数据
 * 3. 切换用户时,必须清空缓存(用户隔离)
 */
function fetchData() {
  // 实现略
}

四、不同层级怎么写?以 Vue 项目为例的一套落地规范

下面从 Vue 项目常见几层结构出发,给一套可直接落地到项目里的注释建议

4.1 组件层(Vue SFC):注释重点放在哪里?

4.1.1 props / emits / expose 是最值得写注释的地方

因为它们构成了组件的“对外接口”。

✅ 示例:表单组件


<script setup lang="ts">
interface Props {
  /**
   * 表单初始值:
   * - 不传则使用内部默认值
   * - 传入时会完全覆盖默认值(不要只传部分字段)
   */
  modelValue?: UserFormModel;

  /**
   * 是否立即在 mounted 后拉取远程选项数据
   * 默认 true;如果父组件要控制时机,可以传 false 后手动调用 `reloadOptions`
   */
  autoLoadOptions?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  autoLoadOptions: true,
});

const emit = defineEmits<{
  /**
   * 表单提交成功时触发
   * payload 包含表单内的所有字段
   */
  (e: 'submit', payload: UserFormModel): void;

  /**
   * 任意字段变化时触发(用于实时保存草稿)
   */
  (e: 'update:modelValue', value: UserFormModel): void;
}>();

defineExpose({
  /**
   * 重新拉取远程下拉选项
   */
  reloadOptions,
});
</script>

这里的注释能让你在不看实现的情况下,就知道怎么用这个组件,这就是高价值注释。

4.1.2 复杂模板逻辑,优先拆组件,其次写块级注释

当模板里出现大量条件判断 / 嵌套 v-if / v-for 时:

  1. 优先选择“拆小组件 / 抽函数”
  2. 仍然复杂时,可以在逻辑块上方加一段块级注释,说明大体意图

✅ 示例:


<template>
  <!-- 展示可见的菜单项:
       1. 已被后端标记为启用
       2. 当前用户有权限
       3. 如果是移动端,只显示前 5 个
  -->
  <MenuItem
    v-for="item in visibleMenuItems"
    :key="item.id"
    :item="item"
  />
</template>

这里注释的作用:

  • 总结了 visibleMenuItems过滤规则
  • 方便别人查找时快速定位逻辑(比如“为什么这个菜单在移动端消失了?”)

4.2 业务逻辑层(hooks / composables / services)

很多 Vue 3 项目会把复杂逻辑拆到:

  • useXXX.ts(逻辑复用)
  • xxxService.ts(调用后端接口 + 业务规则)

这部分逻辑往往最需要注释,但注释也最容易乱写。

4.2.1 统一写在函数/方法签名上方,说明职责和返回值

✅ 示例:组合式函数


/**
 * 订单列表的分页 + 筛选逻辑:
 * - 对外暴露响应式数据:list、loading、pagination
 * - 支持关键字搜索、状态筛选
 * - 初始化时自动加载一次数据
 */
export function useOrderList() {
  const list = ref<Order[]>([]);
  const loading = ref(false);
  const pagination = reactive({
    page: 1,
    pageSize: 20,
    total: 0,
  });

  // ...

  return {
    list,
    loading,
    pagination,
    reload,
    resetFilters,
  };
}

4.2.2 和后端接口交互的地方,注释协议差异/约束

✅ 示例:Service 层


/**
 * 获取订单详情:
 * - 后端只在 status='PAID' 时返回 payInfo 字段
 * - 如果订单已退款,refoundInfo 字段存在但可能为空对象
 * - 接口有 500ms 左右的延迟,注意不要在输入框输入时频繁调用
 */
export async function fetchOrderDetail(orderId: string): Promise<OrderDetail> {
  const { data } = await request.get(`/api/orders/${orderId}`);
  return normalizeOrderDetail(data);
}

这些信息如果不写在这里,很难在代码中第一时间发现,却又对上层调用逻辑影响极大。

4.3 工具层(utils / helpers):何时需要注释?

  • 通用的小工具函数,命名清晰时可以不用注释:

    
    export function formatPrice(amountInCent: number): string { ... }
    
    
  • 如果函数有一些隐含约束或性能特征,就应该注释说明:

✅ 示例:


/**
 * 深拷贝对象(仅用于小对象):
 * - 基于 JSON 序列化,不支持函数 / Date / Map / Set
 * - 遇到循环引用会抛错
 * 适合用于「接口 mock 数据」等简单场景,不要在核心路径频繁使用。
 */
export function simpleClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

五、团队层面的“注释规范建议”:可以直接抄到你们 RULE.md 里

下面给一份可以直接落地的团队规范草稿,你可以根据实际情况微调。

5.1 总体原则

  • P1:注释是代码的一部分,写了就要维护。
  • P2:注释说明“为什么 / 有什么坑 / 有什么约定”,不要“翻译代码”。
  • P3:宁可少写,也不要写错;宁可写在“合适位置”,也不要乱丢。

5.2 “必须注释”的场景

  • 对外接口:
    • 组件的 props / emits / expose
    • 公共工具函数 / Service 层函数的入参、返回值说明(特别是有约束时)
  • 复杂业务逻辑 / 算法:
    • 在函数 / 模块顶部写整体流程说明或规则列表
  • 历史遗留 / 兼容代码:
    • 必须说明历史背景 / 兼容对象 / 计划替换方案
  • TODO / FIXME:
    • 必须写明触发条件 / 影响范围 / 预期目标
    • 建议关联任务号(如:TODO(JIRA-1234)

5.3 “禁止/不鼓励”的注释

  • 重复代码内容的注释(变量名 / 函数名已经表达清楚)
  • 空泛的 TODO / FIXME(未说明问题和上下文)
  • 纯吐槽 / 情绪化注释
  • 长篇大论但没有结构的“感想式注释”

六、一个完整的小案例:从“糟糕注释”到“可维护代码”

下面用一个实际例子,演示如何从“混乱风格”改到“规范易读”。

6.1 初版(很多人项目里真实存在的写法)


<!-- OrderList.vue -->
<script setup lang="ts">
// 订单列表组件

const data = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);

// 获取列表
async function getList() {
  loading.value = true;
  // 调接口
  const res = await request.get('/api/list', {
    params: {
      p: page.value,
      ps: pageSize.value,
    },
  });
  // 处理数据
  data.value = res.data.list;
  total.value = res.data.total;
  loading.value = false;
}

// TODO: 后面要加筛选
</script>

<template>
  <!-- 列表 -->
  <Table :data="data" />
</template>

问题:

  • 命名不清晰(data / getList / /api/list
  • 注释几乎都是废话,没有说明任何约束
  • TODO 没有说明到底怎么“要加筛选”

6.2 改进版:结合命名 + 注释一起升级


<!-- OrderList.vue -->
<script setup lang="ts">
/**
 * 订单列表页:
 * - 支持分页
 * - 计划后续增加:状态筛选、关键字搜索(见 TODO)
 */
import { fetchOrderList } from '@/services/order';

const orders = ref<Order[]>([]);
const loading = ref(false);
const pagination = reactive({
  page: 1,
  pageSize: 10,
  total: 0,
});

/**
 * 拉取订单列表:
 * - 后端的页码从 1 开始(不要传 0)
 * - pageSize 最大不超过 100,否则后端会报错
 */
async function loadOrders() {
  loading.value = true;
  const res = await fetchOrderList({
    page: pagination.page,
    pageSize: pagination.pageSize,
  });
  orders.value = res.list;
  pagination.total = res.total;
  loading.value = false;
}

// TODO(v2.1): 增加筛选条件(状态 / 下单时间区间)
// - 与后端对齐接口 GET /api/orders:新增 status / startAt / endAt 参数
// - UI 上用折叠面板隐藏高级筛选
</script>

<template>
  <OrderTable
    :data="orders"
    :loading="loading"
    :pagination="pagination"
    @change="loadOrders"
  />
</template>

这里我们做了几件事:

  • 改变量名:dataordersgetListloadOrders
  • 提取 Service 层:fetchOrderList(便于复用与测试)
  • 用注释补充约束和未来计划,而不是重复代码

这就是一个**“代码 + 注释配合良好”的例子**。

七、如何把“注释规范”写成一篇能发 CSDN 的文章?

你可以按本文结构,稍作润色,就能产出一篇完整的博客。建议大致结构如下:

  1. 引子(痛点故事)
    • 自嘲+团队真实场景,引出“注释到底该不该写”的问题
  2. 第一原则:好代码优先,注释补充 Why & 限制
  3. 四大高价值注释场景
    • Why / 约定 / 历史坑点 / 复杂流程概览
  4. 四类反面注释示例
    • 重复代码、空 TODO/FIXME、过期注释、碎碎念
  5. 结合 Vue 项目结构的一套实践
    • 组件层、业务层、工具层分别给建议和示例
  6. 前后对比小案例
    • “糟糕版” vs “改进版”
  7. 总结 + 个人习惯分享
    • 比如:写完函数先写注释再实现、Review 时检查注释等

你可以直接把上文复制到 CSDN,稍微调整标题 / 小节顺序,并补充你自己项目中的真实故事和代码片段,会更有代入感和说服力。

八、结语:写给 3 年后的自己

注释不是给现在的你看的,是给“未来的你”和“曾经不认识你的同事”看的。

  • 多写一点“为什么这么写”,少写一点“这行在干嘛”
  • 多写一点“有什么坑 / 有什么约束”,少写一点“将来再说”
  • 写得少,但每一行都值钱,比写一堆废话强太多

技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

❌