原文链接:http://javascript.info/array-methods,translate with ❤️ by zhangbao.

数组提供了许多方法。为了让事情变得更简单,在这一章中,他们被分成小组。

添加/删除元素

我们已经知道了从开头或结尾添加和删除条目的方法:

  • arr.push(…items):添加元素到末尾。
  • arr.pop():从末尾提取元素。
  • arr.shift():从开头提取元素。
  • arr.unshift(…items):在开头添加元素。

这有其他一些方法。

splice

如何从数组中删除一个元素?

数组也是对象,所以可以使用 delete:

  1. let arr = ["I", "go", "home"];
  2. delete arr[1]; // remove "go"
  3. alert( arr[1] ); // undefined
  4. // now arr = ["I", , "home"];
  5. alert( arr.length ); // 3

元素被删除了,但数组还是有 3 个元素,我们能看到 arr.length = 3。

这很自然,因为 delete obj.key 删除 key 的值,这就是它做的,对对象来说是OK的了。但是对于数组,我们通常希望其他元素能够移动并占据这个释放的位置。我们期望现在有一个更短的数组。

因此,应该使用特殊的方法。

arr.splice(str) 方法是用于数组的瑞士军刀。它可以做任何事情:添加、移除和插入元素。

语法是:

  1. arr.splice(index[, deleteCount, elem1, ..., elemN])

我们从 index 这个位置算起,删除 deleteCount 这些个元素,然后在该位置处开始插入元素 elem1, …, elemN。

通过实例,很容易掌握这种方法。

从删除元素开始:

  1. let arr = ["I", "study", "JavaScript"];
  2. arr.splice(1, 1); // from index 1 remove 1 element
  3. alert( arr ); // ["I", "JavaScript"]

容易,对吧?从索引1开始,它删除了1个元素。

在下一个示例中,我们删除了3个元素,并将它们替换为另外两个元素:

  1. let arr = ["I", "study", "JavaScript", "right", "now"];
  2. // remove 3 first elements and replace them with another
  3. arr.splice(0, 3, "Let's", "dance");
  4. alert( arr ) // now ["Let's", "dance", "right", "now"]

在这里,我们可以看到 splice方法返回了移除元素组成的数组:

  1. let arr = ["I", "study", "JavaScript", "right", "now"];
  2. // remove 2 first elements
  3. let removed = arr.splice(0, 2);
  4. alert( removed ); // "I", "study" <-- array of removed elements

splice 方法也可以在没有任何移除的情况下插入元素。为此,我们需要将 deleteCount 设置为 0:

  1. let arr = ["I", "study", "JavaScript"];
  2. // from index 2
  3. // delete 0
  4. // then insert "complex" and "language"
  5. arr.splice(2, 0, "complex", "language");
  6. alert( arr ); // "I", "study", "complex", "language", "JavaScript"

tip: 允许负值索引

在这里和其他数组方法中,允许负索引。他们在数组的末尾指定位置,如下所:

  1. let arr = [1, 2, 5];
  2. // from index -1 (one step from the end)
  3. // delete 0 elements,
  4. // then insert 3 and 4
  5. arr.splice(-1, 0, 3, 4);
  6. alert( arr ); // 1,2,3,4,5

slice

arr.slice 方法比看起来很像的 arr.splice 方法要简单些。

语法是:

  1. arr.slice(start, end)

它返回一个新的数组,它将所有条目开始索引“start”复制到“end”(不包括“end”)。开始和结束都可以是负的,在这种情况下,从数组末端的位置是假定的。

它的工作方式类似于 str.slice,但是它操作返回的是子数组而不是子字符串。

例如:

  1. let str = "test";
  2. let arr = ["t", "e", "s", "t"];
  3. alert( str.slice(1, 3) ); // es
  4. alert( arr.slice(1, 3) ); // e,s
  5. alert( str.slice(-2) ); // st
  6. alert( arr.slice(-2) ); // s,t

concat

arr.concat 方法将数组与其他数组和元素连接起来。

语法是:

  1. arr.concat(arg1, arg2...)

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

结果是一个包含来自arr、arg1、arg2等项的新数组。

如果一个参数是一个数组或者有 Symbol.isConcatSpreadable 属性,然后它的所有元素都会被复制。否则,参数本身被复制。

例如:

  1. let arr = [1, 2];
  2. // merge arr with [3,4]
  3. alert( arr.concat([3, 4])); // 1,2,3,4
  4. // merge arr with [3,4] and [5,6]
  5. alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6
  6. // merge arr with [3,4], then add values 5 and 6
  7. alert( arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6

通常,它只从数组中复制元素(“扩展”它们)。其他对象,即使它们看起来像数组,也会作为一个整体添加:

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

但是如果一个类数字带有 Symbol.isConcatSpreadable 属性的话,添加进去的就是它的元素了:

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

查找数组

这里提供了几个查找元素的方法。

indexOf/lastIndexOf 和 includes

arr.indexOf、arr.lastIndexOf 和 arr.includes 方法有着几乎和字符串同样的语法方法,但是操作的不是字符,而是数组元素。

  • arr.indexOf(item, from) 从 from 索引处开始查找元素,返回查找元素所在的索引值,没有的话返回 -1。
  • arr.lastIndexOf(item, from) 与 indexOf 相似,不过是从末尾开始查找匹配的。
  • arr.includes(item, from) 从 from 索引处开始查找元素,找到的话返回 true,否则返回 false。

例如:

  1. let arr = [1, 0, false];
  2. alert( arr.indexOf(0) ); // 1
  3. alert( arr.indexOf(false) ); // 2
  4. alert( arr.indexOf(null) ); // -1
  5. alert( arr.includes(1) ); // true

注意,方法使用 === 运算符进行比较。因此如果我们查找 false 的话,它找的只是 false 不是 0。

如果我们想要检查是否包含,不想知道确切的索引。可以使用 arr.includes 方法。

includes 方法与 indexOf/lastIndexOf 方法有点不同的是,前者能正确处理 NaN:

  1. const arr = [NaN];
  2. alert( arr.indexOf(NaN) ); // -1 (should be 0, but === equality doesn't work for NaN)
  3. alert( arr.includes(NaN) );// true (correct)

find 和 findIndex

想象下我们有一个对象数组,我们怎么依据特定的判断条件来查找元素呢?

可以用 arr.find 方法。

语法是:

  1. let result = arr.find(function(item, index, array) {
  2. // should return true if the item is what we are looking for
  3. });

这个函数会在数组的每个元素上重复调用:

  • item 当前迭代元素。
  • index 对应索引值。
  • array 被遍历数组本身。

如果结果返回 true,则查找停止,item 返回;如果没找到,返回 undefined。

例如,我们有一组用户,每个用户短对象包含 id 和 name 属性。我们找下 id === 1 的元素吧。

  1. let users = [
  2. {id: 1, name: "John"},
  3. {id: 2, name: "Pete"},
  4. {id: 3, name: "Mary"}
  5. ];
  6. let user = users.find(item => item.id == 1);
  7. alert(user.name); // John

在现实生活中·,对象数组是一个很常见的数据结构,因此 find 方法还是比较有用的。

注意,在例子中我们仅使用了回调函数里的一个参数值 item => item.id === -1。find 函数的其他参数很少使用。

arr.findIndex 方法于此方法类似,但是返回的是查找到的元素的索引值。

filter

find 方法查找单个(第一个)值,也就是函数的返回结果是 true。

如果要返回的是多个值,可以使用 arr.find(fn)

它的语法与 find 大致相同,但是它返回了一个匹配元素的数组:

  1. let results = arr.filter(function(item, index, array) {
  2. // should return true if the item passes the filter
  3. });

例如:

  1. let users = [
  2. {id: 1, name: "John"},
  3. {id: 2, name: "Pete"},
  4. {id: 3, name: "Mary"}
  5. ];
  6. // returns array of the first two users
  7. let someUsers = users.filter(item => item.id < 3);
  8. alert(someUsers.length); // 2

转换数组

本节讨论的是对数组进行转换或重新排序的方法。

map

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

语法是:

  1. let result = arr.map(function(item, index, array) {
  2. // returns the new value instead of item
  3. })

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

例如,在这里我们将每个元素转换为它的长度:

  1. let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length)
  2. alert(lengths); // 5,7,6

sort(fn)

arr.sort 用来对数组排序。

例如:

  1. let arr = [ 1, 2, 15 ];
  2. // the method reorders the content of arr (and returns it)
  3. arr.sort();
  4. alert( arr ); // 1, 15, 2

有没有发现奇怪的地方?

顺序变成 1, 15, 2。不对,但是为什么呢?

在默认情况下,这些项被作为字符串排序。

从字面上看,所有的元素都被转换成字符串,然后进行比较。因此,词典的顺序是应用的,实际上是 “2” > “15”。

为了使用我们自己的排序顺序,我们需要yige1提供两个参数的函数作为 arr.sort() 的参数。

这个函数应该是这样的:

  1. function compare(a, b) {
  2. if (a > b) return 1;
  3. if (a == b) return 0;
  4. if (a < b) return -1;
  5. }

例如:

  1. function compareNumeric(a, b) {
  2. if (a > b) return 1;
  3. if (a == b) return 0;
  4. if (a < b) return -1;
  5. }
  6. let arr = [ 1, 2, 15 ];
  7. arr.sort(compareNumeric);
  8. alert(arr); // 1, 2, 15

现在它按照预期工作。

让我们退一步想想发生了什么。arr 可以是任何东西的数组,对吧?它可能包含数字或字符串或html元素之类的。我们有一组东西。为了对它进行排序,我们需要一个知道如何比较其元素的排序函数。默认是字符串顺序。

sort(fn) 方法有一个内置的排序算法实现。我们不需要关心它到底是如何工作的(大多数时候都是优化的快速排序)。它将遍历数组,使用所提供的功能比较它的元素并重新排序它们,我们所需要的就是提供进行比较的fn。

顺便说一下,如果我们想知道哪些元素是比较的——没有什么可以阻止它们提醒它们:

  1. [1, -2, 15, 2, 0, 8].sort(function(a, b) {
  2. alert( a + " <> " + b );
  3. });

该算法可以在过程中多次比较一个元素,但它尽量少进行比较。

tip: 一个比较函数可以返回任何数字

实际上,一个比较函数只需要返回一个正数来表示“更大”和一个负数来表示“更小”。

这样就可以写更短的函数:

  1. let arr = [ 1, 2, 15 ];
  2. arr.sort(function(a, b) { return a - b; });
  3. alert(arr); // 1, 2, 15

tip: 最好用箭头函数

还记得没有找到“函数表达式”的文章吗?我们可以在这里用它们来进行排序。

  1. arr.sort( (a, b) => a - b );

这和上面的另一个更长的版本是完全一样的。

reverse

arr.reverse 方法反转数组 arr 的元素顺序。

例如:

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

它还在反转之后返回数组 arr。

split 和 join

这是现实生活中的情况。我们正在编写一个消息应用程序,这个人输入了以逗号分隔的接收者列表:John, Pete, Mary。但对我们来说,一组名字会比单个字符串更舒服。如何得到它?

str.split(delim) 方法正式我们现在需要的。它通过给定的定界符 delim 将字符串分割成一个数组。

在下面的例子中,我们用逗号分隔,然后空格:

  1. let names = 'Bilbo, Gandalf, Nazgul';
  2. let arr = names.split(', ');
  3. for (let name of arr) {
  4. alert( `A message to ${name}.` ); // A message to Bilbo (and other names)
  5. }

split方法有一个可选的第二个数值参数——数组长度的限制。如果它被提供,那么额外的元素将被忽略。在实践中,它很少被使用:

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

tip: 分成字母

调用 split(s) 方法时,如果 s 是空字符串,那么会将字符串分割成一系列的字母:

  1. let str = "test";
  2. alert( str.split('') ); // t,e,s,t

arr.join(str) 方法相当于 split 方法的逆向操作。它将数组 arr 中的元素按照指定的连接符连接成一个字符串。

例如:

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

reduce/reduceRight

当我们需要迭代一个数组时——我们可以使用forEach。
当我们需要迭代并返回每个元素的数据时——我们可以使用 map。

arr.reduce 和 arr.reduceRight 方法也属于这类操作,不过更复杂一些。它们被用来根据数组计算单个值。

语法是:

  1. let value = arr.reduce(function(previousValue, item, index, arr) {
  2. // ...
  3. }, initial);

这个函数应用于元素。你可能会注意到那些熟悉的论点,从第二点开始:

  • item 当前的迭代对象。
  • index 当前迭代对象的索引位置。
  • arr 被迭代数组。

到目前为止,像 forEach/map,但它还多一个参数:

  • previousValue 这是调用之前函数返回的值,第一次调用时等于 initial。

最简单的方法就是举个例子。

我们使用一行代码来求数组元素的总和:

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

这里我们使用了最常见的减少变量,它只使用两个参数。

让我们看看发生了什么。

  1. 第一次执行的时候,sum 等于初始值(reduce 的最后一个参数)等于 0,current 是数组的第一个元素,也就是 1,因此结果是 1。
  2. 第二次运行时,sum = 1,让它与数组的第二个参数(2)相加,然后返回。
  3. 第三运行时,sum = 3,让它与数组的第三个元素(3)相加,然后返回……

计算流如下:

数组方法 - 图1

或者以表格的形式,每一行代表的是下一个数组元素的函数调用:

sum current result
第一次调用 0 1 1
第二次调用 1 2 3
第三次调用 3 3 6
第四次调用 6 4 10
第五次调用 10 5 15

正如我们所看到的,前一个调用的结果成为下一个调用的第一个参数。

我们还可以省略初始值:

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

结果是一样的。那是因为如果没有初始值,那么 reduce 就会把数组的第一个元素作为初始值,并从第二个元素开始迭代。

计算表与上面相同,减去第一行。

但这样的使用需要特别注意。如果数组是空的,那么在没有初始值的情况下调用 reduce 会产生错误。

  1. let arr = [];
  2. // Error: Reduce of empty array with no initial value
  3. // if the initial value existed, reduce would return it for the empty arr.
  4. arr.reduce((sum, current) => sum + current);

所以建议总是指定初始值。

arr.reduceRight 方法与此类似,不过是从右往左执行的。

迭代:forEach

arr.forEach 方法允许针对数组的每个元素执行特定的回调函数。

语法:

  1. arr.forEach(function(item, index, array) {
  2. // ... do something with item
  3. });

例如,这里展示了数组里的每个元素:

  1. // for each element call alert
  2. ["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

这段代码更详细地描述了他们在目标数组中的位置:

  1. ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  2. alert(`${item} is at index ${index} in ${array}`);
  3. });

函数的结果(如果它返回any)就会被丢弃并被忽略。

Array.isArray

数组不会形成单独的语言类型。它们是基于对象的。

typeof 运算符并不能帮助区分普通对象和数组:

  1. alert(typeof {}); // object
  2. alert(typeof []); // object

但是数组经常被用到,所以有一个专为判断数组的方法:Array.isArray(value),如果 value 是数组的话,就返回 true,否则返回 false。

  1. alert(Array.isArray({})); // false
  2. alert(Array.isArray([])); // true

许多方法是支持“thisArg”参数的

几乎所有的数组方法,像 find,filter,map,但是排除里外 sort,接收可选参数 thisArg。

这个参数在上面的章节中没有解释,因为它很少被使用。但为了完整性,我们必须覆盖它。

这是这些方法的完整语法:

  1. arr.find(func, thisArg);
  2. arr.filter(func, thisArg);
  3. arr.map(func, thisArg);
  4. // ...
  5. // thisArg is the optional last argument

参数 thisArg 的值称为了 func 中的 this。

例如,在这里我们使用对象方法作为过滤器,thisArg 用起来就很方便:

  1. let user = {
  2. age: 18,
  3. younger(otherUser) {
  4. return otherUser.age < this.age;
  5. }
  6. };
  7. let users = [
  8. {age: 12},
  9. {age: 16},
  10. {age: 32}
  11. ];
  12. // find all users younger than user
  13. let youngerUsers = users.filter(user.younger, user);
  14. alert(youngerUsers.length); // 2

上面的调用中,我们 user.younger 作为一个过滤器,为它提供了 user 作为它的上下文对象 。如果我们不提供上下文,那么 users.filter(user.younger) 会将 user.younger 作为一个独立的函数调用,此时 this=undefined,这立马回导致一个错误。

总结

数组方法一览表:

添加/删除元素:

  • push(…items):向数组末尾添加元素。
  • pop():删除数组的末尾元素。
  • shift():删除数组的开头元素。
  • unshift(…items):在数组开头添加元素。
  • splice(pos, deleteCount, …items):在索引 pos 处删除 deleteCount 个元素,然后插入元素 items。
  • slice(start, end):创建一个新数组,数组成员是位置 start 到 end(不包括)组成的元素集合。
  • concat(…items):返回一个新的数组:复制当前的所有成员并向它添加元素。如果任何项目都是一个数组,那么它的元素就会被取出。

查找元素:

  • indexOf/lastIndexOf(item, pos):从索引位置 pos 处开始查找元素,如果没有找到的话,返回 -1。
  • includes(value):如果数组包含 value 则返回 true,否则返回 false。
  • find/filter(func):通过函数 func 来过滤元素,返回第一个/全部函数里返回 true 的值。
  • findIndex 类似 find 方法,不过返回的是索引而不是元素值。

转换数组:

  • map(func):从为每个元素调用 func 的结果创建一个新的数组。
  • sort(func):排序数组元素,然后返回它。
  • reverse():将数组原地反转,然后返回它。
  • split/join:将字符出转成数组/数组转换为字符串。
  • reduce(func, initial):通过调用每个元素的 func并在调用之间传递一个中间结果来计算数组的单个值。

遍历数组:

  • forEach(func):对数组中每个元素调用函数 func,不返回任何值。

额外:

  • Array.isArray(arr):检查 arr 是不是数组

需要注意的是,sort、reverse 和 splice 方法都会修改数组对象本身。

这些方法是最常用的,它们覆盖了99%的用例。但除此之外几乎没有其他的:

函数fn在数组的每个元素上都被调用,类似于 map。如果任何/所有结果都为真,则返回true,否则返回false。

查看完整列表,请参考文档手册

乍一看,似乎有这么多的方法,很难记住。但实际上这比看起来要容易得多。
仔细检查一下这张列表,只是为了了解它们。然后解决这一章的任务,这样你就有了数组方法的经验。

然后,当你需要用数组做某件事的时候,你不知道怎么做——来这里,看一下备忘单,找到正确的方法。示例将帮助您正确地编写它。很快你就会自动记住这些方法,而不需要你的具体努力。

扩展阅读

(完)