您现在的位置是:网站首页 > 关键逻辑不注释(“能看懂的人自然懂”)文章详情

关键逻辑不注释(“能看懂的人自然懂”)

代码注释的困境

程序员对注释的态度往往两极分化。有人坚持每行代码都要详细注释,有人则认为好代码应该自解释。极端情况下会出现一种现象:关键逻辑完全不注释,美其名曰"能看懂的人自然懂"。这种看似潇洒的做法在实际项目中可能埋下隐患。

自解释代码的局限性

理论上优秀的代码确实应该自我解释。比如:

// 不好的写法
function p(d) {
  return d * 3.14;
}

// 好的写法
function calculatePerimeter(diameter) {
  return diameter * Math.PI;
}

但现实情况要复杂得多。考虑这个React组件:

function UserList({ users }) {
  return (
    <ul>
      {users
        .filter(u => !u.isDisabled)
        .sort((a, b) => a.lastActive - b.lastActive)
        .map(user => (
          <UserItem key={user.id} user={user} />
        ))}
    </ul>
  );
}

这段代码看似清晰,但隐藏了几个关键问题:

  1. 为什么要过滤掉isDisabled的用户?
  2. lastActive的比较顺序是否有特殊考虑?
  3. 为什么不用更高效的排序方式?

业务逻辑的复杂性

真正的复杂度往往来自业务规则。比如电商平台的优惠券计算:

function calculateDiscount(price, coupons, userLevel) {
  let discount = 0;
  
  // 神秘的数字5
  if (coupons.length > 5) {
    coupons = coupons.slice(0, 5);
  }
  
  // 奇怪的优先级
  const validCoupons = coupons.filter(c => {
    return c.expiry > Date.now() && 
          (c.forAll || c.forLevel === userLevel);
  }).sort((a, b) => b.value - a.value);
  
  // 魔法数字0.3
  if (validCoupons.length && userLevel > 3) {
    discount = Math.min(validCoupons[0].value, price * 0.3);
  }
  
  return discount;
}

这段代码至少有3处需要解释:

  1. 为什么限制5张优惠券?
  2. userLevel > 3代表什么?
  3. 30%的折扣上限从何而来?

性能优化的陷阱

性能优化代码尤其需要注释。看这个虚拟滚动实现:

function useVirtualScroll(items, itemHeight, containerRef) {
  const [scrollTop, setScrollTop] = useState(0);
  const viewportHeight = containerRef.current?.clientHeight || 0;
  
  // 魔法数字2
  const bufferSize = 2;
  const startIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
  const endIdx = Math.min(
    items.length - 1,
    Math.ceil((scrollTop + viewportHeight) / itemHeight) + bufferSize
  );
  
  // 为什么需要额外的div?
  const paddingTop = startIdx * itemHeight;
  const paddingBottom = (items.length - endIdx - 1) * itemHeight;
  
  return {
    visibleItems: items.slice(startIdx, endIdx + 1),
    paddingTop,
    paddingBottom
  };
}

没有注释的话,后来者很难理解:

  1. bufferSize为什么是2?
  2. paddingTop/paddingBottom的计算原理
  3. 这种实现方式的性能考量

团队协作的现实

在大型团队中,代码会经过多人之手。一个TypeScript类型定义示例:

type APIResponse<T> = {
  data: T;
  // 为什么需要这个字段?
  _t: number;
  error?: {
    code: string;
    // 这个message是给用户还是开发者?
    message: string;
    details?: unknown;
  };
};

// 特殊处理空数组
type NonEmptyArray<T> = [T, ...T[]];

interface PaginatedResult<T> {
  items: NonEmptyArray<T>;
  // 为什么total可能不存在?
  total?: number;
  // 这个cursor的格式是什么?
  nextCursor?: string;
}

这些类型设计背后的考量如果没有文档,会导致:

  1. _t字段被误用
  2. 错误信息处理不一致
  3. 分页逻辑出现边界case

框架特性的隐式约定

现代前端框架有很多隐式约定。比如Vue的组合式函数:

// useFetch.js
export function useFetch(url, options = {}) {
  const data = ref(null);
  const error = ref(null);
  
  // 为什么不用async/await?
  fetch(url, options)
    .then(res => {
      if (!res.ok) throw new Error(res.statusText);
      return res.json();
    })
    .then(json => (data.value = json))
    .catch(err => (error.value = err));
    
  // 这个返回值顺序有讲究吗?
  return { data, error };
}

问题包括:

  1. 为什么选择then/catch而不是async/await?
  2. 返回值为什么是{ data, error }而不是数组?
  3. 错误处理是否完整?

配置对象的魔法属性

前端配置对象常常包含隐式规则。比如Webpack配置:

module.exports = {
  // 为什么是73?
  hashDigestLength: 73,
  optimization: {
    // 这个配置真的需要吗?
    removeAvailableModules: false,
    splitChunks: {
      // 缓存组命名规则?
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          // 为什么是10?
          priority: 10
        }
      }
    }
  }
};

这些数字和布尔值背后都是血泪教训,但没有注释就会导致:

  1. 配置被随意修改
  2. 新成员不理解优先级
  3. 优化效果大打折扣

时间处理的黑洞

日期时间处理永远充满陷阱。看这个时区转换工具:

function formatDateForAPI(date) {
  // 为什么不用toISOString?
  const year = date.getUTCFullYear();
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
  const day = String(date.getUTCDate()).padStart(2, '0');
  
  // 这个格式后端真的需要吗?
  return `${year}-${month}-${day}T00:00:00Z`;
}

// 特殊的日期解析
function parseSpecialDate(str) {
  // 为什么有这段特殊处理?
  if (str === '0000-00-00') {
    return new Date('1970-01-01');
  }
  
  // 为什么不用new Date(str)?
  const [y, m, d] = str.split('-');
  return new Date(Date.UTC(y, m - 1, d));
}

没有注释的情况下:

  1. 时区处理逻辑容易被破坏
  2. 特殊日期case会被忽略
  3. 格式变更风险增加

状态管理的隐藏规则

状态管理库的使用约定需要明确说明。比如Redux的action creators:

// actionTypes.js
export const FETCH_USER = 'FETCH_USER';
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';

// 为什么需要这个中间action?
export const fetchUser = (userId) => (dispatch, getState) => {
  // 这个检查有必要吗?
  if (getState().users.loading[userId]) {
    return Promise.resolve();
  }
  
  dispatch({ type: FETCH_USER, payload: userId });
  
  // 为什么不直接用async/await?
  return api.getUser(userId)
    .then(user => {
      dispatch({
        type: FETCH_USER_SUCCESS,
        payload: { [userId]: user }
      });
    })
    .catch(error => {
      // 为什么没有FETCH_USER_FAILURE?
      console.error('Fetch user failed:', error);
    });
};

这些设计选择需要解释:

  1. 为什么没有失败action?
  2. 重复请求检查的逻辑
  3. 为什么使用then而不是async/await

样式系统的特殊处理

CSS-in-JS方案也有许多隐含规则。比如Styled-components的主题使用:

const Button = styled.button`
  // 为什么有这两个fallback?
  color: ${({ theme }) => theme.colors?.primary || '#333'};
  padding: ${({ theme }) => theme.spacing?.[2] || '8px'};
  
  // 这个媒体查询有特殊考虑?
  @media (max-width: ${({ theme }) => theme.breakpoints?.md || '768px'}) {
    padding: ${({ theme }) => theme.spacing?.[1] || '4px'};
  }
  
  // 为什么用transparent而不是undefined?
  &:disabled {
    background: transparent;
  }
`;

没有注释会导致:

  1. 主题结构不明确
  2. fallback值随意更改
  3. 响应式断点混乱

测试代码的假设条件

测试代码的上下文尤其需要说明。比如一个React测试:

describe('Tooltip组件', () => {
  // 为什么需要这个mock?
  beforeAll(() => {
    jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
  });
  
  it('应该在hover时显示', async () => {
    // 这个时间为什么是150?
    jest.useFakeTimers();
    render(<Tooltip delay={150} />);
    
    // 为什么需要act?
    act(() => {
      fireEvent.mouseEnter(screen.getByRole('button'));
      jest.advanceTimersByTime(151);
    });
    
    // 这个断言足够吗?
    expect(screen.getByRole('tooltip')).toBeInTheDocument();
  });
});

测试代码的疑问:

  1. requestAnimationFrame为什么要mock?
  2. 151ms的测试时间如何确定?
  3. 为什么需要act包裹?

安全相关的敏感逻辑

安全相关的代码必须要有详细说明。比如一个JWT处理工具:

function parseJWT(token) {
  // 为什么不用split('.')?
  const base64Url = token.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  
  // 这个atob有安全问题吗?
  const jsonPayload = decodeURIComponent(
    atob(base64)
      .split('')
      .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
      .join('')
  );
  
  // 为什么要try/catch?
  try {
    return JSON.parse(jsonPayload);
  } catch {
    // 为什么返回null而不是抛出错误?
    return null;
  }
}

安全敏感代码必须注释:

  1. 替换-和_的原因
  2. URI解码的必要性
  3. 错误处理的考虑

开源项目的文档压力

开源项目的代码更要注重可读性。比如一个工具函数:

/**
 * 深度合并对象,处理循环引用
 * @param {object} target - 目标对象
 * @param {...object} sources - 源对象
 * @returns {object} 合并后的对象
 */
function deepMerge(target, ...sources) {
  const seen = new WeakMap();
  
  function merge(target, source) {
    if (seen.has(source)) {
      return seen.get(source);
    }
    
    seen.set(source, target);
    
    Object.keys(source).forEach(key => {
      const val = source[key];
      
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        if (!target[key]) target[key] = {};
        target[key] = merge(target[key], val);
      } else {
        target[key] = val;
      }
    });
    
    return target;
  }
  
  return sources.reduce(merge, target);
}

开源代码需要:

  1. 清晰的API文档
  2. 算法原理说明
  3. 边界case处理

代码审查的沟通成本

没有注释会增加代码审查难度。比如一个GitHub PR中的改动:

// 旧代码
function formatName(user) {
  return `${user.firstName} ${user.lastName}`;
}

// 新提交
function formatName(user) {
  // 处理东亚姓名顺序
  return user.locale === 'zh' || user.locale === 'ja' 
    ? `${user.lastName}${user.firstName}`
    : `${user.firstName} ${user.lastName}`;
}

如果没有注释说明:

  1. 为什么增加这个逻辑?
  2. 哪些地区适用这个规则?
  3. 是否有更完整的解决方案?

维护者的认知负担

长期维护无注释代码会导致:

// utils.js
function magicSort(arr) {
  // 神秘算法
  return arr.sort((a, b) => {
    const aVal = a.status === 'VIP' ? 0 : 1;
    const bVal = b.status === 'VIP' ? 0 : 1;
    
    if (aVal !== bVal) return aVal - bVal;
    
    // 为什么是乘法?
    return a.score * b.rating - b.score * a.rating;
  });
}

维护者面临的问题:

  1. VIP优先的逻辑是否仍然需要?
  2. 评分算法的依据是什么?
  3. 如何添加新的排序条件?

文档与代码的同步难题

文档与代码不同步也是个问题:

// api.js
/**
 * 获取用户列表
 * @param {number} page - 页码
 * @returns {Promise<User[]>}
 */
export async function getUsers(page = 1) {
  // 实际实现已经改变
  const res = await fetch(`/api/users?page=${page}&limit=20`);
  const { data, total } = await res.json();
  
  // 文档没提到的返回值
  return { 
    users: data,
    hasMore: data.length === 20,
    total
  };
}

这种不一致会导致:

  1. 类型定义错误
  2. 调用方处理不完整
  3. 分页逻辑混乱

注释的艺术

好的注释应该像这样:

// 使用Fisher-Yates算法洗牌
// 注意:会修改原数组
function shuffle(array) {
  let currentIndex = array.length;
  
  while (currentIndex !== 0) {
    // 选取剩余元素
    const randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;
    
    // 交换元素
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex],
      array[currentIndex]
    ];
  }
  
  return array;
}

这样的注释:

  1. 说明算法来源
  2. 指出副作用
  3. 保持与代码同步

注释的负面模式

也要避免不好的注释:

// 不好的注释示例

// 设置默认值
function setName(name) {
  // 如果name不存在
  if (!name) {
    // 设置为默认值
    name = 'Anonymous';
  }
  // 返回name
  return name;
}

问题包括:

  1. 陈述明显的事实
  2. 注释与代码重复
  3. 没有提供额外信息

工具辅助的平衡点

现代工具可以帮助保持平衡:

// TypeScript提供了部分文档
interface PaginationParams {
  /** 当前页码,从1开始 */
  page: number;
  /** 每页数量,默认20 */
  pageSize?: number;
  /** 是否返回总数 */
  includeTotal?: boolean;
}

// JSDoc生成API文档
/**
 * 分页查询产品
 * @param {PaginationParams} params - 分页参数
 * @returns {Promise<{products: Product[], total?: number}>}
 */
async function fetchProducts(params) {
  // ...实现
}

工具化的优势:

  1. 类型即文档
  2. 自动生成API参考
  3. 编辑器智能提示

团队规范的建立

健康的团队应该有注释规范:

# 代码注释规范

## 必须注释的情况
1. 复杂业务逻辑
2. 非常规写法
3. 已知的hack方案
4. 第三方库的特殊用法

## 注释格式要求
- 公共API使用JSDoc
- 内部方法使用行内注释
- 临时代码添加TODO标记

## 示例
/**
 * 计算订单折扣
 * @param {Order} order - 订单对象
 * @param {Coupon[]} coupons - 可用优惠券
 * @returns {number} 折扣金额
 */
function calculateDiscount(order, coupons) {
  // 特殊处理团购订单(业务要求)
  if (order.groupBuyId) {
    return order.total * 0.2; // 固定20%折扣
  }
  
  // TODO: 重构优惠券计算逻辑
  // ...现有实现
}

注释文化的培养

培养团队文化比工具更重要:

  1. 代码审查时检查注释质量
  2. 分享好的注释示例
  3. 定期清理过期注释
  4. 鼓励提问"这段代码为什么这样写"

注释与代码的共生关系

最终目标是达到这种状态:

// 使用ResizeObserver监听元素尺寸变化
// 注意:需要polyfill兼容旧浏览器
function useElementSize(ref) {
  const [size, setSize] = useState(null);
  
  useEffect(() => {
    if (!ref.current) return;
    
    const observer = new ResizeObserver(entries => {
      // 只处理第一个entry(通常只有一个)
      const entry = entries[0];
      if (entry) {
        // 使用contentRect获取纯净尺寸(不含padding/border)
        setSize(entry.contentRect);
      }
    });
    
    observer.observe(ref.current);
    
    return () => observer.disconnect();
  }, [ref]);
  
  return size;
}

这样的代码:

  1. 说明技术选型原因
  2. 指出兼容性要求
  3. 解释实现细节
  4. 保持简洁清晰

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步