我所理解的代码风格最佳实践(1):JavaScript 代码怎样从调用点读下去
- JavaScript 代码怎样从调用点读下去(当前)
- Vue 组件与 composable 的职责边界
- Nuxt 页面、目录与自动导入
- 哪些项目约定值得写成 Lint
假设订单列表要加一个「显示已取消」筛选项。打开页面,发现查询调用是 loadOrders(userId, true, 5000)。第三个参数可能是超时,也可能是条数上限。第二个参数更难猜,甚至得看一遍函数体才能确认它表示「包含已取消」还是「只取已取消」。
让调用点交代这次做什么
const orders = await loadOrders({
accountId,
includeCanceled: true,
timeoutMs: 5000,
});
这里的 accountId 交代订单属于哪个账号,includeCanceled 表明布尔值打开了什么行为,timeoutMs 给出单位。以后加一个可选的 cursor,旧调用可以继续原样使用。要是某个选项改变了既有默认行为,即使形状没变,也要按契约变更看待。
参数形态由调用的含义决定。像 clamp(value, min, max) 这样的底层函数,三个位置都有约定俗成的含义。业务函数里几项可选输入彼此独立时,字段名更能说明这次调用做了什么。一个值来自路由、登录会话还是表单,也值得在名字上区分。账号和订单上都可能有 id,在各自对象内用 id 就够了,同时出现时再叫 accountId 和 orderId。
把默认值写成业务判断
筛选条件里往往同时有 false、0 和空字符串。用 || 统一补默认值,会把这些值一起替换掉。假如约定只在输入缺失时采用默认值,可以写成 includeCanceled ?? false。如果搜索框的空字符串表示清除关键词,就让空字符串继续参与请求构造。输入为空到底意味着清除、沿用还是报错,应在接受输入的地方确定。
同一条筛选规则也尽量由一个地方决定。比如服务端已经根据订单状态决定能否取消,页面可以直接消费这个结果,或者为展示做一次明确的派生。组件各自再维护一份状态判断,产品增加一个订单状态时,就很难确认列表和详情会不会给出不同的操作。
让读者在该停的地方停下来
我倾向把主流程放在调用点附近。只有一次使用的三行字段映射,读者通常可以当场看完。反过来,若列表和导出都要遵守同一套筛选规则,就值得让 buildOrderQuery 成为两条路径的共同入口。它的名字让读者知道这里有一份可复用的业务规则,也让下一次改筛选有明确落点。
导出同样意味着有人可以依赖它。跨模块调用通过明确的公开入口拿 loadOrders,查询参数的编码细节留在模块内部。模块已经分成多个文件时,可以用入口文件挑选对外符号。一个文件就能讲清的模块,直接从该文件导入即可。给每级目录加一个 index.js 转发,通常会延长从调用点到实现的路径。
注释适合留下代码里看不出来的理由。比如兼容旧接口时,normalizeOrderQuery 可以说明它在做转换,真正值得写进注释的是旧接口为何把两个状态合成一个、哪些调用还依赖这个行为,以及何时可以删掉这层转换。实现本身能表达的步骤交给名字和代码顺序。
如果接手的项目已有稳定命名,我会跟着相邻模块写。若某条命名约定确实值得推行,也先从正在改的职责范围开始。至于公开入口的类型、状态枚举和脚本运行方式,见《我所理解的 TypeScript 最佳实践》。