您现在的位置是:网站首页 > 关键逻辑不注释(“能看懂的人自然懂”)文章详情
关键逻辑不注释(“能看懂的人自然懂”)
陈川
【
前端综合
】
3814人已围观
11165字
代码注释的困境
程序员对注释的态度往往两极分化。有人坚持每行代码都要详细注释,有人则认为好代码应该自解释。极端情况下会出现一种现象:关键逻辑完全不注释,美其名曰"能看懂的人自然懂"。这种看似潇洒的做法在实际项目中可能埋下隐患。
自解释代码的局限性
理论上优秀的代码确实应该自我解释。比如:
// 不好的写法
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>
);
}
这段代码看似清晰,但隐藏了几个关键问题:
- 为什么要过滤掉isDisabled的用户?
- lastActive的比较顺序是否有特殊考虑?
- 为什么不用更高效的排序方式?
业务逻辑的复杂性
真正的复杂度往往来自业务规则。比如电商平台的优惠券计算:
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处需要解释:
- 为什么限制5张优惠券?
- userLevel > 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
};
}
没有注释的话,后来者很难理解:
- bufferSize为什么是2?
- paddingTop/paddingBottom的计算原理
- 这种实现方式的性能考量
团队协作的现实
在大型团队中,代码会经过多人之手。一个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;
}
这些类型设计背后的考量如果没有文档,会导致:
- _t字段被误用
- 错误信息处理不一致
- 分页逻辑出现边界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 };
}
问题包括:
- 为什么选择then/catch而不是async/await?
- 返回值为什么是{ data, error }而不是数组?
- 错误处理是否完整?
配置对象的魔法属性
前端配置对象常常包含隐式规则。比如Webpack配置:
module.exports = {
// 为什么是73?
hashDigestLength: 73,
optimization: {
// 这个配置真的需要吗?
removeAvailableModules: false,
splitChunks: {
// 缓存组命名规则?
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
// 为什么是10?
priority: 10
}
}
}
}
};
这些数字和布尔值背后都是血泪教训,但没有注释就会导致:
- 配置被随意修改
- 新成员不理解优先级
- 优化效果大打折扣
时间处理的黑洞
日期时间处理永远充满陷阱。看这个时区转换工具:
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));
}
没有注释的情况下:
- 时区处理逻辑容易被破坏
- 特殊日期case会被忽略
- 格式变更风险增加
状态管理的隐藏规则
状态管理库的使用约定需要明确说明。比如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);
});
};
这些设计选择需要解释:
- 为什么没有失败action?
- 重复请求检查的逻辑
- 为什么使用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;
}
`;
没有注释会导致:
- 主题结构不明确
- fallback值随意更改
- 响应式断点混乱
测试代码的假设条件
测试代码的上下文尤其需要说明。比如一个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();
});
});
测试代码的疑问:
- requestAnimationFrame为什么要mock?
- 151ms的测试时间如何确定?
- 为什么需要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;
}
}
安全敏感代码必须注释:
- 替换-和_的原因
- URI解码的必要性
- 错误处理的考虑
开源项目的文档压力
开源项目的代码更要注重可读性。比如一个工具函数:
/**
* 深度合并对象,处理循环引用
* @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);
}
开源代码需要:
- 清晰的API文档
- 算法原理说明
- 边界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}`;
}
如果没有注释说明:
- 为什么增加这个逻辑?
- 哪些地区适用这个规则?
- 是否有更完整的解决方案?
维护者的认知负担
长期维护无注释代码会导致:
// 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;
});
}
维护者面临的问题:
- VIP优先的逻辑是否仍然需要?
- 评分算法的依据是什么?
- 如何添加新的排序条件?
文档与代码的同步难题
文档与代码不同步也是个问题:
// 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
};
}
这种不一致会导致:
- 类型定义错误
- 调用方处理不完整
- 分页逻辑混乱
注释的艺术
好的注释应该像这样:
// 使用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;
}
这样的注释:
- 说明算法来源
- 指出副作用
- 保持与代码同步
注释的负面模式
也要避免不好的注释:
// 不好的注释示例
// 设置默认值
function setName(name) {
// 如果name不存在
if (!name) {
// 设置为默认值
name = 'Anonymous';
}
// 返回name
return name;
}
问题包括:
- 陈述明显的事实
- 注释与代码重复
- 没有提供额外信息
工具辅助的平衡点
现代工具可以帮助保持平衡:
// 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) {
// ...实现
}
工具化的优势:
- 类型即文档
- 自动生成API参考
- 编辑器智能提示
团队规范的建立
健康的团队应该有注释规范:
# 代码注释规范
## 必须注释的情况
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: 重构优惠券计算逻辑
// ...现有实现
}
注释文化的培养
培养团队文化比工具更重要:
- 代码审查时检查注释质量
- 分享好的注释示例
- 定期清理过期注释
- 鼓励提问"这段代码为什么这样写"
注释与代码的共生关系
最终目标是达到这种状态:
// 使用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;
}
这样的代码:
- 说明技术选型原因
- 指出兼容性要求
- 解释实现细节
- 保持简洁清晰