Here’s how you can make better use of JavaScript arrays
Category(分类): JavaScript Status: 已更新
原文作者:pacdiv 历史文章:Here’s how you can make better use of JavaScript arrays
原文总结了几个数组方法的常见误用。本文保留
indexOf/includes、filter/find、find/some、filter/map/reduce四条主线,同时补充返回值、短路、复杂度、可读性和现代不可变数组方法。方法选择应表达业务意图,不应为了“少遍历一次”机械重写所有代码。
1. 只判断是否包含值:用 includes,需要索引时仍用 indexOf
如果需要知道数组中是否存在某个值,includes 直接返回布尔值:
const fruits = ['apple', 'banana', 'orange']
if (fruits.includes('banana')) {
console.log('banana exists')
}
indexOf 返回第一个匹配元素的索引,适合后续确实需要位置的场景:
const index = fruits.indexOf('banana')
if (index !== -1) {
console.log(`banana is at ${index}`)
}
两者的比较语义也不同:
console.log([NaN].includes(NaN)) // true
console.log([NaN].indexOf(NaN)) // -1
console.log([-0].includes(0)) // true
const sparse = [, undefined]
console.log(sparse.includes(undefined)) // true:空槽按 undefined 处理
console.log(sparse.indexOf(undefined)) // 1:空槽被跳过
includes 使用 SameValueZero;indexOf 使用严格相等风格的查找,因此不能找到 NaN。两者都可以在找到目标后停止遍历,复杂度通常是 O(n),includes 并不是因为返回布尔值就自动更快。
2. 只要第一个匹配元素:用 find,需要全部匹配元素:用 filter
filter 会遍历数组并返回一个新数组,其中包含所有满足条件的元素:
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 2, name: 'Carol' }
]
const usersWithId2 = users.filter(user => user.id === 2)
console.log(usersWithId2.length) // 2
如果业务约束是“ID 唯一”,并且只需要这个元素,find 更能表达意图:
const user = users.find(item => item.id === 2)
console.log(user) // { id: 2, name: 'Bob' }
find 找到第一个匹配项后会停止,找不到时返回 undefined;filter 即使已经找到一个,也要继续检查其余元素,因为它需要收集全部匹配项。二者差异不仅是性能,也是返回值语义:
const missing = users.find(item => item.id === 99)
console.log(missing) // undefined
const missingList = users.filter(item => item.id === 99)
console.log(missingList) // []
不要在只需要一个元素时写 filter(...)[0],这会创建不必要的数组,也隐藏了“只取第一个”的意图。但如果需要确认数据没有重复,filter 返回全部结果反而更适合校验。
3. 只判断是否存在匹配项:用 some,需要元素时用 find
find 返回元素,some 只返回布尔值:
const hasAdmin = users.some(user => user.name === 'Alice')
if (hasAdmin) {
console.log('至少有一个用户匹配')
}
如果后续还要使用匹配的用户,就使用 find,避免再次遍历:
const admin = users.find(user => user.name === 'Alice')
if (admin) {
console.log(admin.id)
}
some 和 find 都会在第一次满足条件时短路;every 则在第一次不满足条件时短路:
const allHaveId = users.every(user => Number.isInteger(user.id))
const noneAnonymous = users.every(user => user.name !== '')
回调不会被调用于已经超出查找范围的元素;数组长度和中途增删元素的细节应以 Array.prototype 的规范语义为准,不要在遍历回调中修改原数组来控制流程。
4. filter 后接 map,还是一次 reduce?
原文建议把下面的两次遍历合并成 reduce:
const values = [1, 2, 3, 4, 5]
const result = values
.filter(value => value % 2 === 0)
.map(value => value * 10)
console.log(result) // [20, 40]
可以写成一次 reduce:
const result = values.reduce((output, value) => {
if (value % 2 === 0) output.push(value * 10)
return output
}, [])
这种写法可能少创建一个中间数组,但不应简单地说“一定性能更好”:
filter+map更直观,便于分别命名和测试两个转换步骤;reduce把筛选和转换混在一起,复杂逻辑可能更难读;- 如果回调中使用扩展运算符反复创建数组,
reduce反而可能更慢; - 小数组的性能差异通常远小于可读性和维护成本;
- 真正的瓶颈应通过基准测试和性能分析确认。
如果使用 reduce,应给累加器明确初始值,避免空数组没有初始值时抛错,也避免依赖首元素类型:
const total = values.reduce((sum, value) => sum + value, 0)
const byId = users.reduce((result, user) => {
result[user.id] = user
return result
}, {})
如果转换步骤已经复杂,保留链式调用并拆成命名函数通常更好:
const isEven = value => value % 2 === 0
const timesTen = value => value * 10
const result = values.filter(isEven).map(timesTen)
5. 现代数组方法补充
原文发布时特别提醒 find 和 includes 不支持 Internet Explorer。今天的现代浏览器和 Node.js 已普遍支持它们,但如果项目仍需兼容旧环境,应使用构建工具/polyfill,并确认 polyfill 的目标范围,而不是只看当前开发机。
1. findLast 和 findLastIndex
较新的运行时提供从数组尾部查找的方法:
const logs = [
{ level: 'info', message: 'start' },
{ level: 'error', message: 'failed' },
{ level: 'info', message: 'retry' }
]
const lastError = logs.findLast(log => log.level === 'error')
const lastErrorIndex = logs.findLastIndex(log => log.level === 'error')
需要兼容旧运行时时应检查支持情况,不能把新方法当作所有浏览器都有。
2. flatMap
当每个元素需要映射为零个、一个或多个元素时,flatMap 可以组合一层 map 和 flat:
const sentences = ['hello world', 'javascript arrays']
const words = sentences.flatMap(sentence => sentence.split(' '))
console.log(words) // ['hello', 'world', 'javascript', 'arrays']
它只展开一层;复杂深度应使用 flat(depth) 或专门的递归逻辑。
3. 不改变原数组的 by-copy 方法
现代 ECMAScript 提供 toSorted()、toReversed()、toSpliced() 和 with(),用于返回新数组而不修改原数组:
const numbers = [3, 1, 2]
const sorted = numbers.toSorted((a, b) => a - b)
const reversed = numbers.toReversed()
const replaced = numbers.with(1, 10)
const removed = numbers.toSpliced(1, 1)
console.log(numbers) // [3, 1, 2]
这些方法需要目标运行时支持;旧环境可以按需使用可靠 polyfill,或使用 slice/展开语法等传统写法。sort()、reverse() 和 splice() 仍然会修改原数组:
const source = [3, 1, 2]
const sortedCopy = [...source].sort((a, b) => a - b)
6. 选择数组方法的快速对照
| 需求 | 推荐方法 | 返回值 |
|---|---|---|
| 判断是否包含某个具体值 | includes | boolean |
| 获取某个具体值的位置 | indexOf/lastIndexOf | 索引或 -1 |
| 获取第一个满足条件的元素 | find/findLast | 元素或 undefined |
| 获取第一个满足条件的索引 | findIndex/findLastIndex | 索引或 -1 |
| 判断是否至少一个元素满足条件 | some | boolean |
| 判断是否所有元素满足条件 | every | boolean |
| 获取全部满足条件的元素 | filter | 新数组 |
| 把每个元素转换成一个结果 | map | 新数组 |
| 逐步汇总为一个值 | reduce | 累加结果 |
| 进行一对多转换并展开一层 | flatMap | 新数组 |
总结
数组方法的最佳实践首先是表达意图:需要布尔值就返回布尔值,需要元素就返回元素,需要全部结果就保留全部结果。短路和中间数组是有用的实现细节,但不应取代正确的语义选择。对性能敏感的代码,应结合数据规模、内存分配、运行时实现和实际 profile 做决定。