你想要的数组方法都在这里啦!

320 阅读11分钟

pop / push, shift / unshift 方法

队列是最常见的使用数组的方法之一。

  • push 在末尾添加一个元素
  • shift 取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。

队列的应用在实践中经常会碰到,例如需要在屏幕上显示消息队列。

数组还有一个用例,就是数据结构——栈。

  • push 在末端添加一个元素
  • pop 从末端取出一个元素

栈通常被被形容成一叠卡片:要么在最上面添加卡片,要么从最上面拿走卡片:

队列先进先出,栈先进后出。JavaScript 中的数组既可以用作队列,也可以用作栈,它们允许你从首端/末端来添加/删除元素。

这在计算机科学中,允许这样的操作的数据结构被称为 双端队列。

  • pop 取出并返回数组最后一个元素
  • push 在数组末端添加元素
  • shift 取出数组第一个元素并返回
  • unshift 在数组首端添加元素

pop 和 unshift 方法都可以一次添加多个元素:

arr.push('a', 'b')
arr.unshift('c', 'd')

数组是一种特殊的对象,使用方括号来访问属性实际上还是来源于对象的语法。

但是数组真正特殊的是它们内部实现,JavaScript 引擎尝试把这些元素一个接一个地存储在连续的内存区域,还有一些其他的优化,以使数组运行得非常快。

但是如果我们像使用常规对象一样使用数组,那么针对数组的优化就不再适用了,然后对应的优化就会被关闭,这些优化所带来的优势也就荡然无存了。

数组误用的几种方式:

  • 添加一个非数字的属性,比如 arr.test=5
  • 制造空洞,比如:添加 arr[0],然后添加 arr[1000](它们中间什么都没有)
  • 以倒叙填充数组,比如 arr[1000], arr[999]等等

性能

push/pop 方法运行的比较快,而 shift/unshift 比较慢

只获取并移除数字 0 对应的元素是不够的。其它元素也需要被重新编号。

shift 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 0,2 改成 1 以此类推,对其重新编号。
  3. 更新 length 属性。

循环

  1. for...of 不能获取当前元素的索引,只是获取元素值,但是大多数情况是够用的。而且这样写更短。
let fruits = ['Apple', 'Orange', 'Plum'];
for(let fruit of fruits) {
    alert( fruit )
}
  1. 技术上来讲,因为数组也是对象,所以使用 for..in 也是可以的:
let arr = ['a', 'b', 'c']
for(let key in arr) {
    alert( arr[key] )
}

但这其实是一个很不好的想法。会有一些潜在问题存在:

  1. for...in 循环会遍历所有属性,不仅仅是这些数字属性。

    类数组对象,看似是数组,有 length 和索引属性,但是也可能有其他的非数字的属性和方法,这通常是我们不需要的。for...in 循环会把他们都列出来。所以如果我们需要处理类数组对象,这些额外的属性就会出现问题。

  2. for...in 循环适用于普通对象,并且做了对应优化。但是不适用于数组,因此速度会慢 10~100 倍。当然即使是这样也非常快,只是在遇到瓶颈时可能会有问题,但我们仍然应该了解这其中的不同。

如何从数组中删除元素

数组是对象,所以我们可以尝试使用 delete,元素被删除,但是长度不变。这很正常,因为 delete obj.key 是通过 key 来移除对应的值。对于对象来说是可以的。但是对于数组来说,我们通常希望剩下的元素可以移动并占据被释放的位置。我们希望得到一个更短的数组。所以应该使用特殊的方法。

splice

语法:

arr.splice(index[, deleteCOunt,elem1,...elemN])

从 index 开始:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已删除元素的数组。

let arr = ['I', 'study', 'javascript'];
arr.splice(1, 1);	// 从索引 1 开始删除 1 个元素
alert( arr );	// ["I", "JavaScript"]

删除 3 个元素,并用另外两个替换

let arr = ["I", "study", "JavaScript", "right", "now"];
arr.splice(0, 3, "Let's", "dance");
console.log( arr );		// ["Let's", "dance", "right", "now"]

将 deleteCount 设置为 0,splice 方法就能够插入元素而不用删除任何元素:

负向索引

let arr = [1, 2, 5];
// 从索引 -1 (尾端前一位) 删除 0 个元素,然后插入 3 和 4
arr.splice(-1, 0, 3, 4)
arr		// [1, 2, 3, 4, 5]

slice

arr.slice 方法比 arr.splice 简单得多。

arr.slice([start], [end])

它会返回一个新数组,将所有从索引 start 到 end(不包括end)的数组项复制到一个新的数组。start 和 end都可以是负数,在这种情况下,从末尾计算索引。

它和字符串的 str.slice 方法有点像,就是把子字符串替换成子数组

let arr = ['t', 'e', 's', 't'];
alert( arr.slice(1, 3) );	// e,s
alert( arr.slice(-2) );		// s,t (复制从位置-2到尾端的元素)

也可以不带参数地调用它:arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。

concat

arr.concat 创建一个新数组,其中包含来自其他数组和其他项的值。

语法:

arr.concat(arg1, arg2...)

它接受任意数量的参数 — 数组或值都可以。

结果是一个包含来自于 arr,然后是 arg1, arg2 的元素的新数组。

如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身

let arr = [1, 2];
alert(arr.concat([3, 4]));	// 1,2,3,4
alert(arr.concat([3, 4], [5, 6]));	// 1,2,3,4,5,6
alert(arr.concat([3, 4], 5, 6));	// 1,2,3,4,5,6

通常,它只复制数组中的元素,其他对象,即使看起来像数组,仍然会被作为一个整体添加:

let arr = [1, 2]
let arrayLike = {
    0: "something",
    length: 1
};
alert(arr.concat(arrayLike));	// 1, 2, [object Object]

...但是,如果类数组对象具有 Symbol.isConcatSpreadable 属性,那么它就会被 concat 当作一个数组来处理:此对象中的元素将被添加:

let arr = [1, 2]
let arrayLike = {
    0: "something",
    1: "else",
    [Symbol.isConcatSpreadable]: true,
    length: 2
};
alert( arr.concat(arrayLike) );	// 1, 2, something, else

遍历:forEach

arr.forEach(function(item,index,array) {
    // ....
})

ES6 提供了三个新的方法 —— entries(), keys() 和 values() —— 用于遍历数组。它们都会返回一个遍历器对象( iterator ),可以用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。

for(let index of ['a', 'b'].keys()){
    console.log(index);	// 0 1
}
for(let elem of ['a', 'b'].values()) {
    console.log(elem);	// 'a' 'b'
}
for(let [index, elem] of ['a', 'b'].entries()){
    console.log(index, elem);	// 0 "a" 1 "b"
}

如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

在数组中搜索

indexOf / lastIndexOf 和 includes

这三个方法与字符串操作具有相同的语法,并且作用基本上也与字符串的方法相同,只不过这里是对数组元素而不是字符进行操作:

  • arr.indexOf( item, from ) 从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1
  • arr.lastIndexOf( item, from ) —— 和上面相同,只是从右向左搜素
  • arr.includes( item, from ) —— 从索引 from 开始搜素 item,找到则返回 true(否则返回 false)
let arr = [1, 0, false];
alert( arr.indexOf(0) );	// 1
alert( arr.indexOf(false) );	// 2
alert( arr.indexOf(null) );		// -1
alert( arr.includes(1) );	// true

请注意,这些方法使用的是严格相等 === 比较。所以如果我们搜索 false,会精确到的确是 false 而不是数字 0。

如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes 是首选。

此外,includes 的一个非常小的差别是他能正确处理 NaN,而不像 indexOf / lastIndexOf:

const arr = [NaN];
alert( arr.indexOf(NaN) );	// -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
alert( arr.includes(NaN) );	// true

find 和 findIndex

想象一下,我们有一个对象数组,我们如何找到具有特定条件的对象?

这时可以用 arr.find 方法。

语法如下:

let result = arr.find(function(item, index, array) {
    // 如果返回 true,则返回 item 并停止迭代
    // 对于 falsy 则返回 undefined
})
// 示例:
let users = [
    {id: 1, name: "John"},
    {id: 2, name: "Pete"},
    {id: 3, name: "Mary"}
]
let user = users.find(item => item.id === 1)
alert(user.name);	// John

在现实生活中,对象数组是很常见的,所以 find 方法非常有用。

注意在这个例子中,我们传给了 find 一个单参数函数 item => item.id == 1。这很典型,并且 find 方法的其他参数很少使用。

arr.findIndex 方法(与 arr.find 方法)基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1。

filter

find 方法搜索的是使函数返回 true 的第一个(单个)元素。

如果需要匹配的有很多,我们可以使用 arr.filter(fn)。

语法与 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组:

let results = arr.filter(function(item, index, array) {
    // 如果 true item 被 push 到 results,迭代继续
    // 如果什么都没找到,则返回空数组
})

例如:

let users = [
    {id: 1, name: "John"},
    {id: 2, name: "Pete"},
    {id: 3, name: "Mary"}
]
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length);	// 2

转换数组

数组转换和重新排序

map

arr.map 方法是最有用和经常使用的方法之一。

它对数组的每个元素都调用函数,并返回结果数组。

let result = arr.map(function(item, index, array) {    
    // 返回新值而不是当前元素
})
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths);	// 5,7,6

sort(fn)

arr.sort 方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)

reverse

arr.reverse 方法用于颠倒 arr 中元素的顺序。

let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr );	// 5, 4, 3, 2, 1

它也会返回颠倒后的数组 arr。

split 和 join

举一个现实生活场景的例子。我们正在编写一个消息应用程序,并且该人员输入以逗号分隔的接收者列表:John, Pete, Mary。但对我们来说,名字数组比单个字符串舒适得多。怎么做才能获得这样的数组呢?

str.split(delim) 方法可以做到。它通过给定的分隔符 delim 将字符串分割成一个数组。

在下面的例子中,我们用“逗号后跟一个空格”作为分隔符:

let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
for(let name of arr) {
    alert(`A message to ${name}.`);	
}

split 方法有一个可选的第二个数字参数 — 对数组长度的限制。如果提供了,那么额外的元素会被忽略。但实际上它很少使用:

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
alert(arr); // Bilbo, Gandalf

调用带有空参数 s 的 split(s),会将字符串拆分为字母数组

arr.join(glue) 与 split 相反。它会在他们之间创建一串由 glue 粘合的 arr 项。

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';')
alert(str); // Bilbo;Gandalf;Nazgul

reduce / reduceRight

它们用于根据数组计算单个值。

let value = arr.reduce(function(accumulator, item, index, array) {
    //...
}, [initial]);

该函数一个接一个地应用于所有的数组元素,并将其结果“搬运”到下一个调用。

参数:

  • accumulator — 是上一个函数调用的结果,第一次等于 inital(如果提供了 initial 的话)。
  • item — 当前的数组元素
  • index — 当前索引
  • arr — 数组本身

应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。

因此,第一个参数本质上是累加器,用于存储所有先前执行的组合结果。最后,它成为 reduce 的结果。

通过一行代码得到一个数组的总和:

let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0)
alert(result); // 15

arr.reduceRight 和 arr.reduce 方法的功能一样,只是遍历为从右到左。

多维数组降维 flat() ,flatMap()

flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

flat() 默认只会“拉平”一层,如果想要"拉平"多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为 1。

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

如果原数组有空位,flat()方法会跳过空位。

[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]

flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map() ),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]

flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])
// flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this。