超级详细的 JS 数组方法详解
Category(分类): JavaScript Status: 未知
数组是 JavaScript 中最常用的数据集合。熟练掌握数组方法,可以提高代码可读性和工作效率;但还要关注方法是否修改原数组、是否跳过稀疏数组空位,以及回调函数的参数和返回值。
一、创建数组
1. 使用数组字面量
数组字面量是最直观、最常用的写法:
const arr4 = []
const arr5 = [20]
const arr6 = ['lily', 'lucy', 'Tom']
console.log(arr4.length) // 0
console.log(arr5.length) // 1,而不是“长度为 20”
console.log(arr6.length) // 3
2. 使用 Array 构造函数
无参构造
const arr1 = new Array()
console.log(arr1) // []
带参构造
new Array(length) 只有在恰好传入一个非负整数时才表示创建指定长度的稀疏数组,数组中并没有对应数量的实际元素。传入多个参数,或传入一个非数字参数,则会把参数作为元素:
const arr2 = new Array(20)
console.log(arr2.length) // 20
console.log(0 in arr2) // false,索引 0 是空位
const arr3 = new Array('lily', 'lucy', 'Tom')
const arr4 = new Array('23')
console.log(arr3) // ['lily', 'lucy', 'Tom']
console.log(arr4) // ['23']
日常代码中通常优先使用数组字面量,避免 new Array(value) 在单参数场景下产生歧义。
3. Array.of(ES2015)
Array.of() 总会把所有参数作为数组元素,不会把单个数字当成数组长度:
const arr = Array.of(1, 2)
console.log(arr.length) // 2
const arr1 = Array.of(3)
console.log(arr1.length) // 1
console.log(arr1[0]) // 3
const arr2 = Array.of('2')
console.log(arr2.length) // 1
console.log(arr2[0]) // '2'
4. Array.from(ES2015)
Array.from() 可以把可迭代对象或类数组对象转换为新数组。它是浅转换,不会递归克隆元素:
function arga(...args) {
const arg = Array.from(args)
console.log(arg)
}
arga('arr1', 26, 'from') // ['arr1', 26, 'from']
映射转换
第二个参数是映射函数,会在生成目标数组时对每个元素进行转换:
function arga(...args) {
return Array.from(args, value => `${value}1`)
}
const arr = arga('arr', 26, 'pop')
console.log(arr) // ['arr1', '261', 'pop1']
如果映射函数需要使用对象作为 this,可以传入第三个参数:
const helper = {
diff: 1,
add(value) {
return typeof value === 'number' ? value + this.diff : `${value}${this.diff}`
}
}
function translate() {
return Array.from(arguments, helper.add, helper)
}
const arr = translate('liu', 26, 'man')
console.log(arr) // ['liu1', 27, 'man1']
二、数组方法
数组原型方法概览
join():用分隔符把元素连接为字符串push()/pop():从数组末尾添加/删除元素shift()/unshift():从数组开头删除/添加元素slice():返回一段浅复制,不修改原数组splice():在原数组上删除、插入或替换元素fill():用一个值填充指定范围,修改原数组filter():筛选元素,返回新数组concat():连接数组或值,返回新数组indexOf()/lastIndexOf():按严格相等查找索引every()/some():全部满足/至少一个满足includes():按 SameValueZero 判断是否包含值sort():排序并修改原数组reverse():反转并修改原数组forEach():遍历但不产生新数组,不能用break提前结束map():映射并返回新数组reduce()/reduceRight():归并为一个值copyWithin():在数组内部复制,修改原数组find()/findIndex():查找第一个满足条件的值/索引toLocaleString()/toString():转换为字符串flat()/flatMap():扁平化一层或多层数组entries()/keys()/values():取得数组迭代器
现代运行时还提供 at()、findLast()、findLastIndex(),以及不修改原数组的 toReversed()、toSorted()、toSpliced()、with() 等方法,文末会补充。
各个方法的基本功能详解
1. join()
join() 会把数组元素转换为字符串,并使用指定分隔符连接,默认分隔符是逗号。undefined、null 元素会按空字符串处理,方法不会修改原数组:
const arr = [1, 2, 3]
console.log(arr.join()) // 1,2,3
console.log(arr.join('-')) // 1-2-3
console.log(arr) // [1, 2, 3]
通过 join() 可以实现重复字符串;生产代码也可以直接使用更语义化的 String.prototype.repeat():
function repeatString(str, n) {
return new Array(n + 1).join(str)
}
console.log(repeatString('abc', 3)) // abcabcabc
console.log('Hi'.repeat(5)) // HiHiHiHiHi
2. push() 和 pop()
push() 从数组末尾添加一个或多个元素,并返回新长度;pop() 删除并返回最后一个元素:
const arr = ['Lily', 'lucy', 'Tom']
const count = arr.push('Jack', 'Sean')
console.log(count) // 5
console.log(arr) // ['Lily', 'lucy', 'Tom', 'Jack', 'Sean']
const item = arr.pop()
console.log(item) // Sean
console.log(arr) // ['Lily', 'lucy', 'Tom', 'Jack']
两者都会修改原数组;在频繁操作超大数组的场景中,还要考虑开头/末尾操作的性能和数据结构选择。
3. shift() 和 unshift()
shift() 删除并返回第一个元素;unshift() 从开头添加元素并返回新长度。这两个方法都会修改原数组,开头操作通常需要移动其他索引:
const arr = ['Lily', 'lucy', 'Tom']
const count = arr.unshift('Jack', 'Sean')
console.log(count) // 5
console.log(arr) // ['Jack', 'Sean', 'Lily', 'lucy', 'Tom']
const item = arr.shift()
console.log(item) // Jack
console.log(arr) // ['Sean', 'Lily', 'lucy', 'Tom']
4. sort()
sort() 默认按字符串的 UTF-16 编码单元升序排序,因此数字数组必须提供比较函数。它会修改原数组。现代 ECMAScript 实现从 ES2019 起要求排序稳定,但比较函数仍应尽量满足可传递性等规则:
const arr1 = ['a', 'd', 'c', 'b']
console.log(arr1.sort()) // ['a', 'b', 'c', 'd']
const arr2 = [13, 24, 51, 3]
console.log(arr2.sort()) // [13, 24, 3, 51]
console.log(arr2) // 原数组已改变
比较函数返回负数表示 a 排在 b 前,返回 0 表示等价,返回正数表示 a 排在 b 后:
function compare(value1, value2) {
return value1 - value2
}
const arr2 = [13, 24, 51, 3]
console.log(arr2.sort(compare)) // [3, 13, 24, 51]
降序可以交换参数:
const arr2 = [13, 24, 51, 3]
console.log(arr2.sort((a, b) => b - a)) // [51, 24, 13, 3]
5. reverse()
reverse() 颠倒元素顺序并修改原数组:
const arr = [13, 24, 51, 3]
console.log(arr.reverse()) // [3, 51, 24, 13]
console.log(arr) // [3, 51, 24, 13]
6. concat()
concat() 连接一个或多个数组和值,返回新数组,不修改原数组。数组参数默认只展开一层,因此它是浅操作:
const arr = [1, 3, 5, 7]
const arrCopy = arr.concat(9, [11, 13])
console.log(arrCopy) // [1, 3, 5, 7, 9, 11, 13]
console.log(arr) // [1, 3, 5, 7]
const arrCopy2 = arr.concat([9, [11, 13]])
console.log(arrCopy2) // [1, 3, 5, 7, 9, [11, 13]]
console.log(arrCopy2[5]) // [11, 13]
对象也可以通过 Symbol.isConcatSpreadable 控制是否被展开,这是少见的高级用法。
7. slice()
slice(start, end) 返回从 start(包含)到 end(不包含)的浅复制,不修改原数组。负数会从数组末尾倒数,超出范围会被规范化到有效边界:
const arr = [1, 3, 5, 7, 9, 11]
const arrCopy = arr.slice(1)
const arrCopy2 = arr.slice(1, 4)
const arrCopy3 = arr.slice(1, -2) // 等价于 arr.slice(1, 4)
const arrCopy4 = arr.slice(-4, -1) // 等价于 arr.slice(2, 5)
console.log(arr) // [1, 3, 5, 7, 9, 11]
console.log(arrCopy) // [3, 5, 7, 9, 11]
console.log(arrCopy2) // [3, 5, 7]
console.log(arrCopy3) // [3, 5, 7]
console.log(arrCopy4) // [5, 7, 9]
8. splice()
splice(start, deleteCount, ...items) 会修改原数组,并返回被删除元素组成的数组,可以删除、插入或替换。
删除元素
const arr = [1, 3, 5, 7, 9, 11]
const arrRemoved = arr.splice(0, 2)
console.log(arr) // [5, 7, 9, 11]
console.log(arrRemoved) // [1, 3]
向指定索引处添加元素
const array1 = [22, 3, 31, 12]
const removed = array1.splice(1, 0, 12, 35)
console.log(removed) // []
console.log(array1) // [22, 12, 35, 3, 31, 12]
替换指定索引位置的元素
const array1 = [22, 3, 31, 12]
const removed = array1.splice(1, 1, 8)
console.log(removed) // [3]
console.log(array1) // [22, 8, 31, 12]
9. indexOf() 和 lastIndexOf()
两者接收要查找的值和可选的起始索引,使用严格相等比较,因此不会找到 NaN;未找到时返回 -1。lastIndexOf 从后向前查找:
const arr = [1, 3, 5, 7, 7, 5, 3, 1]
console.log(arr.indexOf(5)) // 2
console.log(arr.lastIndexOf(5)) // 5
console.log(arr.indexOf(5, 2)) // 2
console.log(arr.lastIndexOf(5, 4)) // 2
console.log(arr.indexOf('5')) // -1
console.log([NaN].indexOf(NaN)) // -1
10. forEach()
forEach() 对每个实际存在的元素调用回调,回调参数依次是 element、index、array。它返回 undefined,会跳过稀疏数组空位,不能用 break 提前结束;需要产生新数组时使用 map,需要提前结束时可以考虑 for...of、some 或普通 for。
const arr = [11, 22, 33, 44, 55]
arr.forEach((value, index, array) => {
console.log(`${value}|${index}|${array === arr}`)
})
// 11|0|true
// 22|1|true
// 33|2|true
// 44|3|true
// 55|4|true
forEach 是 ES5 方法,不是“ES5 及以下”的旧循环语法;它在现代浏览器和 Node.js 中都很常用。
11. map()
map() 是 ES5 方法,返回一个新数组,回调参数与 forEach 相同,不修改原数组。它会跳过原数组空位,并在结果中保留对应空位:
const arr = [1, 2, 3, 4, 5]
const arr2 = arr.map(item => item * item)
console.log(arr2) // [1, 4, 9, 16, 25]
console.log(arr) // [1, 2, 3, 4, 5]
12. filter()
filter() 对每个实际存在的元素执行回调,把回调结果为真值的元素放入新数组:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const arr2 = arr.filter((value, index) => index % 3 === 0 || value >= 8)
console.log(arr2) // [1, 4, 7, 8, 9, 10]
13. fill()(ES2015)
fill(value, start, end) 用同一个值填充范围,包含 start、不包含 end,并修改原数组。若填充的是对象,所有位置会共享同一个对象引用:
const arr = [1, 2, 3, 'cc', 5]
arr.fill(1)
console.log(arr) // [1, 1, 1, 1, 1]
const arr2 = [1, 2, 3, 'arr', 5]
arr2.fill(1, 2)
console.log(arr2) // [1, 2, 1, 1, 1]
arr2.fill(0, 1, 3)
console.log(arr2) // [1, 0, 0, 1, 1]
const objects = Array(2).fill({ count: 0 })
objects[0].count++
console.log(objects[1].count) // 1,共享引用
14. every()
every() 只有在所有元素都满足条件时才返回 true;空数组会返回 true,这是“没有反例”的逻辑结果:
const arr = [1, 2, 3, 4, 5]
console.log(arr.every(value => value < 10)) // true
console.log(arr.every(value => value < 3)) // false
console.log([].every(Boolean)) // true
15. some()
some() 只要有一个元素满足条件就返回 true,空数组返回 false:
const arr = [1, 2, 3, 4, 5]
console.log(arr.some(value => value < 3)) // true
console.log(arr.some(value => value < 1)) // false
console.log([].some(Boolean)) // false
16. includes()(ES2016)
includes(value, fromIndex) 判断数组是否包含指定值,使用 SameValueZero 比较,因此 NaN 与自身相等,-0 与 0 也相等:
const array1 = [22, 3, 31, 12, 'arr']
console.log(array1.includes(31)) // true
console.log(array1.includes(31, 3)) // false,从索引 3 开始查找
const values = [1, NaN, 2]
console.log(values.indexOf(NaN)) // -1
console.log(values.includes(NaN)) // true
17. reduce() 和 reduceRight()
reduce() 从左到右处理,reduceRight() 从右到左处理。回调通常接收累加器、当前值、当前索引和原数组;第二个参数是初始累加值。没有初始值时,首个元素会作为初始累加值,空数组会抛出 TypeError:
const values = [1, 2, 3, 4, 5]
const sum = values.reduceRight((prev, cur) => prev + cur, 10)
console.log(sum) // 25
const product = values.reduce((prev, cur) => prev * cur, 1)
console.log(product) // 120
18. toLocaleString() 和 toString()
toString() 通常以逗号连接元素;toLocaleString() 会把每个元素按当前区域设置转换,日期、数字和嵌套数组的结果可能随环境和 locale 改变:
const array1 = [22, 3, 31, 12]
console.log(array1.toLocaleString()) // 22,3,31,12(结果受 locale 影响)
console.log(array1.toString()) // 22,3,31,12
19. find() 和 findIndex()
两者都接收回调和可选的 thisArg,回调参数为元素、索引和数组;找到第一个满足条件的元素后停止。find 返回值,找不到返回 undefined;findIndex 返回索引,找不到返回 -1:
const arr = [1, 2, 3, 'arr', 5, 1, 9]
console.log(arr.find(value => value > 2)) // 3
console.log(arr.findIndex(value => value > 2)) // 2
console.log(arr.find(value => value > 10)) // undefined
20. copyWithin()(ES2015)
copyWithin(target, start, end) 在数组内部复制元素,包含 start、不包含 end,会修改原数组,并且复制过程能正确处理重叠范围:
const arr = [1, 2, 3, 'arr', 5]
arr.copyWithin(3, 0)
console.log(arr) // [1, 2, 3, 1, 2]
const arr2 = [1, 2, 3, 'arr', 5, 9, 17]
arr2.copyWithin(3, 0, 3)
console.log(arr2) // [1, 2, 3, 1, 2, 3, 17]
21. flat() 和 flatMap()(ES2019)
flat(depth) 按指定深度递归展开并返回新数组,默认深度为 1;Infinity 可以展开任意嵌套深度。它还会跳过被展开层级中的空位:
const arr1 = [0, 1, 2, [3, 4]]
console.log(arr1.flat()) // [0, 1, 2, 3, 4]
const arr2 = [0, 1, 2, [[[3, 4]]]]
console.log(arr2.flat(2)) // [0, 1, 2, [3, 4]]
const arr3 = [1, 2, [3, 4, [5, 6, [7, 8]]]]
console.log(arr3.flat(Infinity)) // [1, 2, 3, 4, 5, 6, 7, 8]
const sparse = [1, 2, , 4]
console.log(sparse.flat()) // [1, 2, 4]
flatMap() 相当于先 map(),再以深度 1 执行 flat(),不能一次展开任意深度:
const values = [2, 3, 4]
const result = values.flatMap(value => [value, value * 2])
console.log(result) // [2, 4, 3, 6, 4, 8]
22. entries()、keys() 和 values()(ES2015)
这三个方法都返回数组迭代器:keys() 遍历索引,values() 遍历值,entries() 遍历 [index, value]:
for (const index of ['a', 'b'].keys()) {
console.log(index)
}
// 0
// 1
for (const elem of ['a', 'b'].values()) {
console.log(elem)
}
// a
// b
for (const [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem)
}
// 0 a
// 1 b
也可以手动调用迭代器的 next():
const letters = ['a', 'b', 'c']
const entries = letters.entries()
console.log(entries.next().value) // [0, 'a']
console.log(entries.next().value) // [1, 'b']
console.log(entries.next().value) // [2, 'c']
console.log(entries.next()) // { value: undefined, done: true }
三、现代数组方法补充
1. at()
at(index) 支持负索引,适合读取末尾元素;它不会修改数组:
const values = [10, 20, 30]
console.log(values.at(0)) // 10
console.log(values.at(-1)) // 30
console.log(values.at(99)) // undefined
2. findLast() 和 findLastIndex()
这两个方法从数组末尾开始查找:
const values = [1, 2, 3, 2]
console.log(values.findLast(value => value % 2 === 0)) // 2
console.log(values.findLastIndex(value => value % 2 === 0)) // 3
3. 不修改原数组的复制方法
toReversed()、toSorted()、toSpliced() 和 with() 是 ES2023 的“复制后修改”方法,适合不可变数据风格:
const values = [3, 1, 2]
const sorted = values.toSorted((a, b) => a - b)
const reversed = values.toReversed()
const replaced = values.with(1, 9)
const spliced = values.toSpliced(1, 1, 8)
console.log(values) // [3, 1, 2]
console.log(sorted) // [1, 2, 3]
console.log(reversed) // [2, 1, 3]
console.log(replaced) // [3, 9, 2]
console.log(spliced) // [3, 8, 2]
如果目标浏览器或 Node.js 版本较旧,应通过兼容性数据或构建目标确认支持情况;不要仅根据“支持 ES6”推断支持所有后续数组方法。
4. Array.fromAsync()
现代运行时还提供 Array.fromAsync(),可以把异步可迭代对象、同步可迭代对象或类数组对象转换为 Promise 数组结果。它适合需要按顺序等待异步元素的场景:
async function demo() {
const result = await Array.fromAsync([1, 2, 3], async value => value * 2)
console.log(result) // [2, 4, 6]
}
demo()
5. 选择方法时的建议
- 只读访问优先考虑
at、find、findLast等方法。 - 需要改变原数组时使用
push、splice、sort等,并在函数命名或注释中明确副作用。 - 需要不可变更新时优先使用
toSorted、toSpliced、with,或使用展开运算符创建浅副本。 - 数组只包含一层引用时,
slice、concat、展开、Array.from都只是浅复制;需要深复制时,应根据数据类型和兼容目标选择structuredClone或专门的序列化方案。 - 不要把稀疏数组空位和显式
undefined混为一谈;不同方法对空位的处理不同。
参考资料
- MDN:Array
- MDN:Array.prototype.sort()
- MDN:Array.prototype.toSorted()
- MDN:Array.prototype.at()
- MDN:Array.from()
- MDN:Array.of()
作者:Yushia
链接:https://juejin.cn/post/6907109642917117965
来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。