返回博客

让代码自己说话——编码规范背后的工程哲学

「编码规范」这个词很容易让人昏昏欲睡。它让人联想到 lint 报错、PR 里鸡毛蒜皮的注释、以及「你的缩进不对」之类的争论。我刚开始写代码时也这么觉得——规范是给大公司、大团队用的形式主义,自己写东西只要能跑就行。

后来接手过几个「能跑但谁都不敢动」的代码库,才真正理解规范想解决的问题:代码是写一次、读 N 次的东西,而那个「N 次」里,大多数时候读的人是几个月后的你自己。

可读性优先,不是风格偏好

规范里最不性感、也最有价值的一条是:变量和函数名要能说清楚它在干什么。

// 这两行逻辑完全一样,但认知负担差一个数量级
const x = await api(id)
const marketDetail = await fetchMarketById(id)

fetchMarketByIdapi 多打了十几个字,但它消灭了一个心智跳转:你不需要追溯调用链就知道这里在做什么。函数命名用「动词+名词」的形式,一方面是规范,另一方面是在强迫你想清楚这个函数的职责边界——如果你发现很难给它起名,往往意味着它做的事情不够单一。

KISS(Keep It Simple, Stupid)和 YAGNI(You Aren't Gonna Need It)是同一件事的两面:前者说「现在的逻辑别写复杂」,后者说「未来的需求别提前写」。过度抽象和提前泛化是我见过最常见的「聪明陷阱」——代码看起来很优雅,但当真正的需求来了,那套抽象往往和它对不上,反而要全部推翻。

不可变性:防御式编程的基础

TypeScript/JavaScript 里有一条让很多人觉得「多此一举」的规范:永远不要直接修改对象或数组,用展开运算符创建新的副本。

// 这样写看起来更简洁
user.name = 'New Name'
items.push(newItem)
 
// 但这样写才是安全的
const updatedUser = { ...user, name: 'New Name' }
const updatedItems = [...items, newItem]

为什么?因为 JavaScript 的对象和数组是引用传递。你在函数 A 里修改了 user.name,函数 B 里持有同一个引用的代码也被悄悄改了。这种「幽灵修改」在小项目里可能没问题,但一旦有多个地方同时依赖同一个对象状态(React 的 state、Redux store、或者任何共享数据结构),直接 mutation 就是 bug 的温床。

不可变性不是函数式编程的专利,它是一种防御性思维:我不信任自己的副作用,所以我把副作用显式化。

注释解释 Why,不解释 What

好的注释是解释「为什么这样做」,而不是「做了什么」——后者代码自己就说了。「用指数退避避免 API 过载」是有价值的注释;「将计数器加一」是噪音。注释越少越好,但每一条都该值它占据的行。

早返回比深嵌套好读十倍

这是我改变最多实际代码习惯的一条:用「卫语句」(guard clause)提前返回,而不是用多层 if-else 嵌套包裹核心逻辑。

// 深嵌套版本,核心逻辑埋在第五层
if (user) {
  if (user.isAdmin) {
    if (market) {
      if (market.isActive) {
        // 真正要做的事在这里
      }
    }
  }
}
 
// 早返回版本,一眼就能看到正常路径
if (!user) return
if (!user.isAdmin) return
if (!market?.isActive) return
 
// 真正要做的事在这里,没有缩进噪音

深嵌套的本质问题是:它把「异常路径」和「正常路径」混在一起,让读者无法快速定位核心逻辑在哪里。 早返回把异常路径全部收到函数开头处理掉,核心逻辑就自然留在「干净」的上下文里了。

异步代码也是同理——能并行的请求用 Promise.all 并发,而不是一个接一个等。这不只是性能问题,也是在代码结构上表达「这几件事之间没有依赖关系」的意图。

取舍与边界

规范不是教条。有时候直接 mutation 确实更高效(处理超大数组时),有时候内联逻辑比抽出一个函数更清晰(极短的一次性转换)。

判断的标准始终是:这段代码三个月后被陌生人(或者失忆的你自己)读到,能不能在五秒内理解它在干什么、为什么这样写? 如果能,规范可以灵活;如果不能,就该回头改。

规范的本质不是让代码「符合标准」,而是让代码承载意图——让写代码的思考过程能够被读代码的人复现。

一句话心法

代码给机器运行,也给人阅读;好的规范是在替未来的读者省力气,不是在给现在的自己加负担。