注释规范
渡一教育-Web前端开发
2026年08月07日 06:00

作者:谢杰

该文章是《开发规范》系列文章的第三篇。

前面我们有介绍过,规范根据是否和代码相关,分类为:

  • 非编码类规范:主要包括开源规范、文档规范、CommitMessage规范和版本规范

  • 编码类规范:目录规范、代码规范、注释规范、接口规范、日志规范和错误码规范

这篇文章我们来看一下编码类规范中的其中一种:注释规范

注释的核心价值

在软件开发中,注释不仅是代码的补充,更是一种“隐形资产”。它能拉近开发者之间的沟通距离,降低理解和维护的门槛,并将经验沉淀为团队可长期复用的知识。接下来,我们来看看注释所承载的核心价值。

1. 团队协作的黏合剂

程序最终会被计算机执行,但日常更重要的读者是人类。注释是开发者之间最直接的交流方式,主要面对以下场景:

  • 未来的自己(半年后再看,可能完全不记得当初的逻辑)

  • 项目同伴(他们没经历你写代码时的思路)

  • 后续维护者(可能完全不熟悉你的业务语境)

  • 新加入的伙伴(需要在最短时间内融入代码体系)

缺乏注释的代码,就像一份说明书被撕掉的复杂设备,它确实能用,但没有人知道哪些按钮不能乱按,风险极高。

2. 注释应回答“为什么”

写注释的首要价值,不是重复解释代码逻辑,而是记录当初做出技术选择的原因。代码本身能展示 做了什么(What),但只有注释能解释 为什么要这样做(Why)

代码块
PlainText
自动换行
复制代码
// 为什么这里没有用缓存?
function fetchUserProfile(id) {
  // 直接请求接口而非本地缓存,原因如下:
  // 1. 用户资料更新频率高,缓存容易过期
  // 2. 登录后需保证数据最新,避免展示旧头像/昵称
  // 3. 接口响应时间稳定,可接受
  return api.get(`/user/${id}`);
}
复制成功

如果没有这些注释,下一个维护的人可能会“贴心”地加上缓存,结果却导致用户数据经常显示不一致。

再比如:

代码块
PlainText
自动换行
复制代码
// 为什么选择这个算法?
function processData(data) {
  // 使用冒泡排序而不是快速排序,因为:
  // 1. 数据量很小(<100条),性能差异可忽略
  // 2. 冒泡排序代码更简单,易于维护
  // 3. 快速排序在这个特定场景下有不稳定的风险
  return bubbleSort(data);
}
复制成功

没有这个注释,其他开发者可能会"优化"成快速排序,反而引入问题。

3. 减轻理解负担

当业务逻辑或算法较为复杂时,单靠阅读代码,开发者往往需要在脑中构建一套心智模型。而注释的作用,就是帮别人快速搭建这套模型。

代码块
PlainText
自动换行
复制代码
// 这个函数处理订单生命周期
// 状态流转:CREATED → PAID → SHIPPED → (DELIVERED | CANCELED)
// 特殊情况:CANCELED 订单在未发货时允许全额退款
function handleOrderLifecycle(currentStatus, action) {
  // 内部包含大量条件分支逻辑...
}
复制成功

花 30 秒看注释,比花 30 分钟硬啃代码要高效得多。注释就是理解复杂系统的“快速通道”。

4. 减少踩坑的概率

高质量的注释不仅仅是说明逻辑,还能提前提醒后续开发者,避免一些隐藏的问题:

  • 边界条件提示:例如注意:当输入为 null 时将返回默认值

  • 副作用说明:例如 调用此方法会写入数据库,请谨慎使用

  • 已知缺陷标记:例如 TODO: 多线程环境下存在并发问题

代码块
PlainText
自动换行
复制代码
function sendEmail(to, content) {
  // ⚠️ 注意:测试环境下不会真正发送邮件,只会写入日志
  // 生产环境配置见 ops/email-config.yaml
  // FIXME: 如果收件人地址包含特殊字符,可能导致编码异常
  return emailService.send(to, content);
}
复制成功

这些注释就像“路标和警示牌”,帮助后来者避开陷阱,降低 bug 出现的概率。再比如:

代码块
PlainText
自动换行
复制代码
function calculateDiscount(price, userType) {
  // 注意:VIP用户的折扣计算方式不同,因为历史原因
  // 见业务需求文档 BRD-2023-045
  if (userType === 'VIP') {
    return price * 0.7; // 特殊的30%折扣
  }
  return price * 0.9; // 普通用户10%折扣
}
复制成功

5. 沉淀团队经验

注释不仅服务当下的阅读者,它还是团队的“集体记忆”。 当资深开发者离开时,留下的注释往往能帮新成员快速理解设计背后的考量。

代码块
PlainText
自动换行
复制代码
// 注意:此接口的重试间隔必须是 200ms
// 原因:上游支付网关 QPS 限制为 5 次/秒
// 200ms = 1000ms / 5,保证不会触发风控
const RETRY_INTERVAL = 200;
复制成功

这样的注释本质上就是“经验的传递”,避免团队在相同的坑上反复试错。

6. 提升代码评审效率

在 Code Review 环节,注释能起到“导航”的作用:

  • 让审查者更快把握作者的设计意图

  • 帮助识别潜在的逻辑漏洞

  • 对比实现与注释是否一致,从而验证代码是否真正符合预期

有注释的代码就像带说明的设计稿,让评审者专注于判断方案是否合理,而不是浪费时间去猜作者的思路。

如何写注释

接下来我们来看一下该如何写注释。

1. 保持注释的鲜活度与可靠性

原则:一条错误的注释,往往比没有注释更具破坏性。

代码会随着迭代不断演进,而注释却常常被遗忘。过时的信息会误导阅读者,带来不必要的排查和沟通成本。

实践要点:

  • 把注释当作代码逻辑的一部分:修改实现时,必须同步检查并更新相关注释,这是开发者应有的职业习惯。

  • 定期清理过时注释:在 Code Review 或重构过程中,主动标记并修正可疑或陈旧的注释,必要时干脆删除。

反面示例:

代码块
PlainText
自动换行
复制代码
// 使用 setTimeout 模拟异步调用,避免阻塞
// (注:后来代码已经改为使用 Promise,但注释没更新)
async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("done"), 1000);
  });
}
复制成功

这里的注释已经与实际实现脱节,不仅失去价值,还会误导维护者。

2. 注释要解释“为什么”,而不是复述“做了什么”

写注释时,最常见的误区就是写“流水账”式注释:只是简单重复代码在做的事,却没有告诉读者为什么要这么做。这种注释不但没有价值,还会增加维护成本(因为代码一改,注释也要改,否则就会产生误导)。

2-1. 避免“流水账”式注释(What 注释)

不好的示例:

代码块
PlainText
自动换行
复制代码
// 定义计数器
let count = 0;

// 遍历数组
for (let i = 0; i < items.length; i++) {
  // 打印元素
  console.log(items[i]);
}

// 返回两数相加的结果
function add(a, b) {
  // 返回 a 和 b 相加的结果
  return a + b;
}
复制成功

这些注释完全是重复代码行为,任何开发者一眼就能看懂。它们不仅无用,还会在代码变更时变成负担。

2-2. 解释“为什么”(Why)

这是注释的核心价值。它要回答的是“为什么要这么写”,背后可能是性能考量、历史原因、业务规则或 Bug 规避。

好的示例:

代码块
PlainText
自动换行
复制代码
// 使用 Map 代替数组索引查找,因为数据量超过 1w 条,必须保证 O(1) 查询性能
const userMap = new Map(users.map(u => [u.id, u]));

// 设置超时时间为 90 秒,因为外部 API 响应不稳定,短于此时间容易失败
const TIMEOUT = 90000;

// 不能直接用浮点数计算,避免 IEEE 754 精度问题
const total = Math.round(amount * 100) / 100;
复制成功

2-3. 解释“约束”(Constraints)

有些代码必须依赖特定前提才能正常运行,注释应明确说明。

代码块
PlainText
自动换行
复制代码
// 必须在用户已登录的情况下调用,否则会抛出权限错误
function loadUserProfile() { ... }

// 受数据库字段限制,最大只能存储 128 个字符
const MAX_TAG_LENGTH = 128;
复制成功

2-4. 解释“契约”(Contract)

注释还可以作为“简化版 API 文档”,说明函数输入、输出和行为规范。

代码块
PlainText
自动换行
复制代码
/**
 * 计算订单折扣
 * @param {Order} order - 订单对象,包含 basePrice 与 customer
 * @returns {number} - 返回折扣后的价格,保证结果 ≥ 0
 */
function getDiscountPrice(order) { ... }
复制成功

2-5. 解释“副作用”(Side Effects)

一些代码除了完成主要功能,还会附带隐含影响,必须明确写在注释里。

代码块
PlainText
自动换行
复制代码
// 此方法会直接修改传入的数组(原地排序),而不是返回新数组
function sortInPlace(arr) { ... }

// 调用该接口不仅会创建用户,还会触发欢迎邮件和短信发送(参见任务单 TKT-1024)
api.createUser(newUser);
复制成功

总结:代码只会告诉读者 What(做了什么),而注释应该补充:

  1. Why(为什么这么做)

  2. Constraints(限制条件)

  3. Contract(约定)

  4. Side Effects(副作用)

这样,注释才能真正帮助未来的维护者理解和延续你的设计思路。

3. 善用统一的标记

原则:用一致的关键字标记待办、问题和风险点,让团队快速理解代码现状。

这种方式简单直接,既能帮助自己记住后续要处理的地方,也能让其他人一眼看出代码中有哪些“隐患”。

常见标记约定:

  • TODO:表示后续需要补充的功能或优化点

  • FIXME:表示已知 Bug 或待修复的代码,优先级通常高于 TODO

  • XXX:表示虽然能运行,但存在风险,需要特别关注

  • HACK:表示临时性解决方案,为应对特殊情况而写

示例:

代码块
PlainText
自动换行
复制代码
// TODO: 下个版本发布前需要替换成新接口
function fetchLegacyData() {
  // ...
}

// FIXME: 这里在并发请求下可能产生数据不一致,需要加锁机制
function updateSession() {
  // ...
}
复制成功

这种标记就像在代码中插上“小旗子”,告诉未来的你和团队成员:这里还有事没完,别掉以轻心。

4. 使用规范的文档注释

原则:函数、类和模块的注释应当结构化、可读性强。

与其零散地写说明,不如采用统一的文档注释规范,比如 JSDoc。这样不仅方便团队成员理解,还能配合工具自动生成 API 文档,减少维护成本。

实践要点:

  • 在公共方法、核心模块和对外暴露的 API 上必须添加 JSDoc

  • 明确说明输入参数的类型、含义,以及返回值的格式

  • 对边界情况、默认值、可能抛出的错误做出标注

  • 避免长篇大论,用简洁的短句直击关键信息

示例:

代码块
PlainText
自动换行
复制代码
/**
 * 根据用户ID获取用户信息
 * @param {string} userId - 用户唯一标识
 * @param {Object} [options] - 可选配置
 * @param {boolean} [options.includePosts=false] - 是否加载用户的帖子
 * @returns {Promise<User>} - 返回一个用户对象
 * @throws {NotFoundError} 当用户不存在时抛出
 */
async function getUser(userId, options) {
  // ...
}
复制成功

这种结构化的注释方式,不仅帮助他人快速理解函数意图,还能直接作为“活文档”,提升代码的可维护性和专业度。

5. 为复杂逻辑或算法添加必要注释

原则:当代码实现方式不是一眼能看懂的,或者使用了巧妙但晦涩的技巧,就必须写清楚注释。

实践要点:

  • 解释算法:说明用了哪种算法,或为什么要选择它

  • 解释“魔法数字”:出现固定数值或特殊字符串时,应定义为常量,并注明含义和来源

  • 阐明意图:让读者知道你想解决什么问题,而不仅仅是看到代码在执行的细节

示例:

代码块
PlainText
自动换行
复制代码
/**
 * 使用二分查找在有序数组中定位目标值
 * 这样比线性查找更高效:O(log n) vs O(n)
 * 特殊处理:若数组中有重复值,返回第一个出现的位置
 */
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let result = -1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      result = mid;
      // 继续向左查找,确保拿到第一个出现的索引
      right = mid - 1;
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return result;
}
复制成功

这里的注释清楚解释了:

  • 用二分查找的原因(性能优势)

  • 特殊边界处理(重复值时的策略)

  • 算法目标(不仅找到目标,还要返回第一个出现的位置)

再比如:

代码块
PlainText
自动换行
复制代码
/**
 * 为搜索输入框设置防抖功能,避免频繁触发搜索请求
 * 解决用户快速输入时导致的性能问题和API过度调用
 */
function setupSearchInput(inputElement, onSearch) {
  let timeoutId = null; // 存储setTimeout的ID,用于取消之前的等待

  inputElement.addEventListener('input', (event) => {
    const searchValue = event.target.value.trim();

    // 如果已有等待中的计时器,立即清除它
    // 这样确保只有最后一次输入会真正触发搜索
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // 为空值时立即返回,不发起搜索请求
    // 提升用户体验并减少不必要的API调用
    if (searchValue === '') {
      onSearch('');
      return;
    }

    // 设置新的计时器:等待300ms后执行搜索
    // 这个时间间隔是用户体验和响应速度的平衡点:
    // - 太短:防抖效果不佳,仍然会频繁请求
    // - 太长:用户会觉得搜索响应迟钝
    timeoutId = setTimeout(() => {
      onSearch(searchValue);
      timeoutId = null; // 执行完成后清理ID
    }, 300);
  });

  // 添加额外处理:当输入框失去焦点时立即搜索
  // 即使用户输入后没有等待防抖时间直接点击搜索按钮
  inputElement.addEventListener('blur', () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
      onSearch(inputElement.value.trim());
    }
  });
}
复制成功

这里的注释帮助阅读者快速理解:

  • 为什么要加防抖(避免频繁请求,提升性能与用户体验)

  • 300ms 的时间间隔是如何权衡得出的(体验与响应速度之间的平衡)

  • 特殊边界场景的处理方式(空输入、失去焦点等)

所以,复杂逻辑的注释,不是为了“翻译代码”,而是帮助后来者迅速理解你当时的设计意图与取舍。

写在最后

在写注释时,不妨先快速问自己几个问题:

  1. 这条注释是否提供了代码本身看不出来的信息?(特别是“为什么”)

  2. 它是否与最新代码保持一致,而不是过时信息?

  3. 是否解释了复杂、特殊或不直观的实现细节?

  4. 它能否帮助他人(或者未来的自己)更快理解和使用这段代码?

  5. 如果把这条注释删掉,代码会不会变得晦涩难懂?

如果答案大多是“是”,那说明这是一条有价值的注释。记住:代码是给机器执行的,但更是给人阅读和理解的。而注释,就是你向后来者传递思路、经验和设计意图的最好方式。