我所理解的 JavaScript 最佳实践(4):按遍历任务选择循环和数组方法
看到 tasks[index],我会再扫一遍循环体,看看这段代码是不是真的用到了位置。如果 index 只在取元素时出现,直接遍历任务就够了。下标和步长参与操作时,再保留普通的 for。
逐项执行操作用 for…of
下面按列表顺序为任务登记曝光。登记只需要当前任务的 ID。
for (let index = 0; index < tasks.length; index += 1) {
recordImpression(tasks[index].id);
}
去掉下标后,读者在循环头就能看见每轮处理一条任务。
for (const task of tasks) {
recordImpression(task.id);
}
forEach 也能逐项执行同步操作。我通常用 for…of,因为循环体可以直接用 continue 跳过当前项,用 break 结束整个循环。比如按优先级尝试打开任务,先跳过禁用项,成功打开一项就停止。
for (const task of tasks) {
if (task.disabled) {
continue;
}
if (await openTask(task)) {
break;
}
}
在 forEach 的回调里写 return 只会结束本次回调。它也不会等待异步回调返回的 Promise。批量提交怎样等待请求,可以接着看第五篇的异步遍历。
转换和查找用直接返回结果的方法
map 表达的是把每项转换成一个结果。例如任务列表要显示标签,可以直接写 const labels = tasks.map(task => task.title)。看到 map 就能预期拿到一个新数组,回调负责说明每个标签怎样产生。要留下所有符合条件的项,就用 filter。
只想找一条记录时,也可以把「索引循环、条件、保存结果、break」交给数组方法。例如 A、B、C、D 四条记录里,B 和 D 已过期。以下写法共用同一个条件。
const isExpired = item => item.status === 'expired';
const hasExpired = items.some(isExpired); // true
const firstExpired = items.find(isExpired); // B
const expiredItems = items.filter(isExpired); // B、D
some 回答有没有,find 取第一条,两者在 B 命中就停。filter 检查剩余记录,拿到全部匹配项。没有匹配时,三者分别返回 false、undefined 和空数组。把 filter(...)[0] 改成 find 前,确认回调只做判断,原先每项都执行的日志或更新会随提前停止一起减少。
自有对象字段先取键、值或键值对
for…in 会枚举对象及其原型链上可枚举的字符串键。多数业务对象只需要自己的字段。这时可以直接从想要的结果出发。Object.keys 取键,Object.values 取值,Object.entries 取键值对,三者都只取自有、可枚举的字符串键所对应的属性。
for (const [field, value] of Object.entries(payload)) {
recordField(field, value);
}
这也省去了在 for…in 的每轮手动调用 Object.hasOwn。如果业务确实需要枚举继承字段,才保留 for…in。ECMAScript 的对象枚举方法
其他常用操作也能直接表达结果。项目已有 es-toolkit 时,可以用 uniq 给标签去重,用 sum 加总一组数字。已有键值对时,用 Object.fromEntries 转成对象,同键会保留后写入的值。更多分组、计数和建索引的写法见第三篇《不用 reduce》。
es-toolkit 的 uniq · es-toolkit 的 sum
下标参与操作时保留普通 for
列表是 [A, B, C, D],B 和 C 相邻且都过期。如果调用方要求保留原数组的引用,可以从后往前删除。
for (let index = tasks.length - 1; index >= 0; index -= 1) {
if (tasks[index].expired) {
tasks.splice(index, 1);
}
}
先在下标 2 删除 C,再在下标 1 删除 B,最终剩下 [A, D]。如果从前往后删 B 后直接递增下标,C 会左移到刚处理过的位置,被跳过去。允许创建新数组时,可以用 filter 保留未过期的任务。
while 在第一轮前检查条件
队列里有 A、B、C,这次最多执行两条。开始前先检查队列和剩余预算。预算一开始就是 0 时,A 也不会被取出来。
let remaining = 2;
while (queue.length > 0 && remaining > 0) {
const task = queue.shift();
await runTask(task);
remaining -= 1;
}
这里的队列是只存任务的普通密集数组,检查长度和取出任务之间没有等待。A 和 B 各消耗一份预算,C 留在队列里。类似地,沿着父节点逐层向上查找时也可以用 while。每一轮只取下一个父节点,走到根就结束。
do…while 先执行一次再决定是否继续
打开任务历史页时,虽然还没有游标,第一页也必须请求。收到响应之后,才知道是否有下一页。
let cursor: string | undefined;
do {
const page = await fetchPage(cursor);
appendItems(page.items);
cursor = page.nextCursor;
} while (cursor !== undefined);
这个接口用 undefined 请求第一页,也用它表示没有下一页。第一页给出 'p2' 就继续,否则一轮结束。读这种循环时,可以直接沿着游标看下一轮由什么触发。队列预算、父节点和游标都在运行中变化,和固定走过一组数组下标是两种不同的循环条件。