技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

Here’s how you can make better use of JavaScript arrays

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/19-Here’s how you can make better use of JavaScript a
本文目录8 个章节
  1. 1. 只判断是否包含值:用 includes,需要索引时仍用 indexOf
  2. 2. 只要第一个匹配元素:用 find,需要全部匹配元素:用 filter
  3. 3. 只判断是否存在匹配项:用 some,需要元素时用 find
  4. 4. filter 后接 map,还是一次 reduce?
  5. 5. 现代数组方法补充
  6. 6. 选择数组方法的快速对照
  7. 总结
  8. 参考资料

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/includesfilter/findfind/somefilter/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 找到第一个匹配项后会停止,找不到时返回 undefinedfilter 即使已经找到一个,也要继续检查其余元素,因为它需要收集全部匹配项。二者差异不仅是性能,也是返回值语义:

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)
}

somefind 都会在第一次满足条件时短路;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. 现代数组方法补充

原文发布时特别提醒 findincludes 不支持 Internet Explorer。今天的现代浏览器和 Node.js 已普遍支持它们,但如果项目仍需兼容旧环境,应使用构建工具/polyfill,并确认 polyfill 的目标范围,而不是只看当前开发机。

1. findLastfindLastIndex

较新的运行时提供从数组尾部查找的方法:

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 可以组合一层 mapflat

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. 选择数组方法的快速对照

需求推荐方法返回值
判断是否包含某个具体值includesboolean
获取某个具体值的位置indexOf/lastIndexOf索引或 -1
获取第一个满足条件的元素find/findLast元素或 undefined
获取第一个满足条件的索引findIndex/findLastIndex索引或 -1
判断是否至少一个元素满足条件someboolean
判断是否所有元素满足条件everyboolean
获取全部满足条件的元素filter新数组
把每个元素转换成一个结果map新数组
逐步汇总为一个值reduce累加结果
进行一对多转换并展开一层flatMap新数组

总结

数组方法的最佳实践首先是表达意图:需要布尔值就返回布尔值,需要元素就返回元素,需要全部结果就保留全部结果。短路和中间数组是有用的实现细节,但不应取代正确的语义选择。对性能敏感的代码,应结合数据规模、内存分配、运行时实现和实际 profile 做决定。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS