我所理解的 JavaScript 最佳实践(10):一行代码只做一件事
滚动区可以用已有标题作为无障碍名称。有标题 ID 时写入 aria-labelledby,标题被清空时移除旧属性。我在读 Marker 的滚动区组件时看到,两条分支各占一行,判断和 DOM 操作挤在一起。
if (title) element.setAttribute('aria-labelledby', title);
else element.removeAttribute('aria-labelledby');
我会把开括号接在 if 或 else 后面,让块内动作从下一行开始。第一次读可以先扫条件,确定要看哪条路,再往里读这条路修改了什么。
if (title) {
element.setAttribute('aria-labelledby', title);
} else {
element.removeAttribute('aria-labelledby');
}
缩进看起来属于分支,执行时可能已经出了分支
假设滚动节点为空时,原来只需要提前返回。后来还要在返回前断开观察器,往原来的卫语句里加一行,很容易写成下面这样。
function measureViewport(viewport, observer) {
if (!viewport)
observer.disconnect()
return
return viewport.getBoundingClientRect()
}
if 只管紧接着的 observer.disconnect()。即使 viewport 存在,下一行的 return 也会结束函数,最后的测量永远执行不到。大括号提前圈出了卫语句的边界,新增清理动作时,原来的 return 仍留在分支里。
function measureViewport(viewport, observer) {
if (!viewport) {
observer.disconnect();
return;
}
return viewport.getBoundingClientRect();
}
Douglas Crockford 在 2005 年的《The Elements of JavaScript Style》里,用 Yahoo 的 Cookie 读取代码做过一个类似的演示。把后面的索引调整也缩进到单句 if 下面,缩进看似把它划入分支,JavaScript 仍会照常执行它。他给出的建议是「Always use blocks in structured statements.」
Google 的 JavaScript 风格指南也把非空块的第一条语句放到 { 后的新行,并保留 } else { 的连写。它允许足够短、没有 else 的简单 if 省略块。我在项目里连这种卫语句也加括号。维护代码时,一行 return 很容易变成「记录原因再返回」或「清理资源再返回」。
一行可以选一个值
滚动区还要处理 aria-label 和 role。如果把三组判断都展开,读者每次都要辨认「这个属性为什么有值」和「属性该设置还是移除」。可以先把名称规则写成值,再交给同一个 DOM 工具。下面的 setOrRemoveAttribute 来自 sheng-tool。给字符串就写入,给 null 就移除。
import { setOrRemoveAttribute } from 'sheng-tool';
function syncViewportName(viewport, label, titleId) {
const ariaLabelledBy = titleId.trim() || null;
const ariaLabel = ariaLabelledBy ? null : label.trim() || null;
setOrRemoveAttribute(viewport, 'aria-labelledby', ariaLabelledBy);
setOrRemoveAttribute(viewport, 'aria-label', ariaLabel);
setOrRemoveAttribute(viewport, 'role', ariaLabelledBy || ariaLabel ? 'region' : null);
}
ariaLabelledBy ? null : label.trim() || null 这行只负责决定 aria-label 的值。标题 ID 优先,文字标签兜底。三元表达式的两个分支都在产出这个值,DOM 写入另起一行。读到调用处,也能直接确认每次操作的是哪个属性。
把条件触发的动作和执行顺序写出来
saved && notify() 可以只在保存成功时发通知。这儿没有后续代码需要读取表达式的结果,读者要找的是通知什么时候会发出。用 if 写明触发条件后,通知动作也有了可以加日志或断点的位置。
function reportSaveResult(saved, notify) {
if (saved) {
notify();
}
}
同样,frozen ? '已冻结' : missingDocuments ? '缺材料' : '可审核' 压在一行时,读者要先找到第二个 ? 属于哪一支。如果这里还承担检查优先级,就把两条提前返回分别写出来。先冻结、再查材料,和业务规定的顺序一致。 《卫语句让主流程保持平直》
function getReviewLabel(frozen, missingDocuments) {
if (frozen) {
return '已冻结';
}
if (missingDocuments) {
return '缺材料';
}
return '可审核';
}
没有条件分支也可能值得拆开。renderRows(normalizeRows(await loadRows())) 把等待请求、整理数据和更新界面套在一起。调试列表为空时,我想先看服务端有没有返回记录,再看转换丢了哪些记录,最后才查界面。这时两个只用一次的变量刚好给出了两处检查点。 《尽量去掉只用一次的中间变量》
async function refreshRows(loadRows, normalizeRows, renderRows) {
const rawRows = await loadRows();
const rows = normalizeRows(rawRows);
renderRows(rows);
}
用 ESLint 要求大括号,用 Prettier 排版
ESLint 的 curly: ['error', 'all'] 会报告省略大括号的控制语句,并可自动修复。如果已经用了 eslint-config-prettier,把这条规则放在兼容配置后面。Prettier 会排开已有块里的语句,省略的大括号则由 ESLint 补齐。
import prettier from 'eslint-config-prettier/flat';
export default [prettier, { rules: { curly: ['error', 'all'] } }];
接手旧代码时,可以先用 eslint --fix 补上块,再跑一次 Prettier。代码审查时,把注意力留给工具看不出的事,例如两条检查谁先执行、哪一步会改状态、异步失败后还有哪些动作会继续。