原文链接:http://javascript.info/array-methods,translate with ❤️ by zhangbao.
数组提供了许多方法。为了让事情变得更简单,在这一章中,他们被分成小组。
添加/删除元素
我们已经知道了从开头或结尾添加和删除条目的方法:
- arr.push(…items):添加元素到末尾。
- arr.pop():从末尾提取元素。
- arr.shift():从开头提取元素。
- arr.unshift(…items):在开头添加元素。
这有其他一些方法。
splice
如何从数组中删除一个元素?
数组也是对象,所以可以使用 delete:
let arr = ["I", "go", "home"];
delete arr[1]; // remove "go"
alert( arr[1] ); // undefined
// now arr = ["I", , "home"];
alert( arr.length ); // 3
元素被删除了,但数组还是有 3 个元素,我们能看到 arr.length = 3。
这很自然,因为 delete obj.key 删除 key 的值,这就是它做的,对对象来说是OK的了。但是对于数组,我们通常希望其他元素能够移动并占据这个释放的位置。我们期望现在有一个更短的数组。
因此,应该使用特殊的方法。
arr.splice(str) 方法是用于数组的瑞士军刀。它可以做任何事情:添加、移除和插入元素。
语法是:
arr.splice(index[, deleteCount, elem1, ..., elemN])
我们从 index 这个位置算起,删除 deleteCount 这些个元素,然后在该位置处开始插入元素 elem1, …, elemN。
通过实例,很容易掌握这种方法。
从删除元素开始:
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // from index 1 remove 1 element
alert( arr ); // ["I", "JavaScript"]
容易,对吧?从索引1开始,它删除了1个元素。
在下一个示例中,我们删除了3个元素,并将它们替换为另外两个元素:
let arr = ["I", "study", "JavaScript", "right", "now"];
// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // now ["Let's", "dance", "right", "now"]
在这里,我们可以看到 splice方法返回了移除元素组成的数组:
let arr = ["I", "study", "JavaScript", "right", "now"];
// remove 2 first elements
let removed = arr.splice(0, 2);
alert( removed ); // "I", "study" <-- array of removed elements
splice 方法也可以在没有任何移除的情况下插入元素。为此,我们需要将 deleteCount 设置为 0:
let arr = ["I", "study", "JavaScript"];
// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");
alert( arr ); // "I", "study", "complex", "language", "JavaScript"
tip: 允许负值索引
在这里和其他数组方法中,允许负索引。他们在数组的末尾指定位置,如下所:
let arr = [1, 2, 5];
// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);
alert( arr ); // 1,2,3,4,5
slice
arr.slice 方法比看起来很像的 arr.splice 方法要简单些。
语法是:
arr.slice(start, end)
它返回一个新的数组,它将所有条目开始索引“start”复制到“end”(不包括“end”)。开始和结束都可以是负的,在这种情况下,从数组末端的位置是假定的。
它的工作方式类似于 str.slice,但是它操作返回的是子数组而不是子字符串。
例如:
let str = "test";
let arr = ["t", "e", "s", "t"];
alert( str.slice(1, 3) ); // es
alert( arr.slice(1, 3) ); // e,s
alert( str.slice(-2) ); // st
alert( arr.slice(-2) ); // s,t
concat
arr.concat 方法将数组与其他数组和元素连接起来。
语法是:
arr.concat(arg1, arg2...)
它接受任意数量的参数——数组或值。
结果是一个包含来自arr、arg1、arg2等项的新数组。
如果一个参数是一个数组或者有 Symbol.isConcatSpreadable 属性,然后它的所有元素都会被复制。否则,参数本身被复制。
例如:
let arr = [1, 2];
// merge arr with [3,4]
alert( arr.concat([3, 4])); // 1,2,3,4
// merge arr with [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6
// merge arr with [3,4], then add values 5 and 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]
//[1, 2, arrayLike]
但是如果一个类数字带有 Symbol.isConcatSpreadable 属性的话,添加进去的就是它的元素了:
let arr = [1, 2];
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
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。
例如:
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 方法与 indexOf/lastIndexOf 方法有点不同的是,前者能正确处理 NaN:
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (should be 0, but === equality doesn't work for NaN)
alert( arr.includes(NaN) );// true (correct)
find 和 findIndex
想象下我们有一个对象数组,我们怎么依据特定的判断条件来查找元素呢?
可以用 arr.find 方法。
语法是:
let result = arr.find(function(item, index, array) {
// should return true if the item is what we are looking for
});
这个函数会在数组的每个元素上重复调用:
- item 当前迭代元素。
- index 对应索引值。
- array 被遍历数组本身。
如果结果返回 true,则查找停止,item 返回;如果没找到,返回 undefined。
例如,我们有一组用户,每个用户短对象包含 id 和 name 属性。我们找下 id === 1 的元素吧。
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 方法还是比较有用的。
注意,在例子中我们仅使用了回调函数里的一个参数值 item => item.id === -1。find 函数的其他参数很少使用。
arr.findIndex 方法于此方法类似,但是返回的是查找到的元素的索引值。
filter
find 方法查找单个(第一个)值,也就是函数的返回结果是 true。
如果要返回的是多个值,可以使用 arr.find(fn)。
它的语法与 find 大致相同,但是它返回了一个匹配元素的数组:
let results = arr.filter(function(item, index, array) {
// should return true if the item passes the filter
});
例如:
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
转换数组
本节讨论的是对数组进行转换或重新排序的方法。
map
arr.map 方法是最常使用和有用的方法。
语法是:
let result = arr.map(function(item, index, array) {
// returns the new value instead of item
})
它调用数组中的每个元素的函数,并返回结果数组。
例如,在这里我们将每个元素转换为它的长度:
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length)
alert(lengths); // 5,7,6
sort(fn)
arr.sort 用来对数组排序。
例如:
let arr = [ 1, 2, 15 ];
// the method reorders the content of arr (and returns it)
arr.sort();
alert( arr ); // 1, 15, 2
有没有发现奇怪的地方?
顺序变成 1, 15, 2。不对,但是为什么呢?
在默认情况下,这些项被作为字符串排序。
从字面上看,所有的元素都被转换成字符串,然后进行比较。因此,词典的顺序是应用的,实际上是 “2” > “15”。
为了使用我们自己的排序顺序,我们需要yige1提供两个参数的函数作为 arr.sort() 的参数。
这个函数应该是这样的:
function compare(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
例如:
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
let arr = [ 1, 2, 15 ];
arr.sort(compareNumeric);
alert(arr); // 1, 2, 15
现在它按照预期工作。
让我们退一步想想发生了什么。arr 可以是任何东西的数组,对吧?它可能包含数字或字符串或html元素之类的。我们有一组东西。为了对它进行排序,我们需要一个知道如何比较其元素的排序函数。默认是字符串顺序。
sort(fn) 方法有一个内置的排序算法实现。我们不需要关心它到底是如何工作的(大多数时候都是优化的快速排序)。它将遍历数组,使用所提供的功能比较它的元素并重新排序它们,我们所需要的就是提供进行比较的fn。
顺便说一下,如果我们想知道哪些元素是比较的——没有什么可以阻止它们提醒它们:
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( a + " <> " + b );
});
该算法可以在过程中多次比较一个元素,但它尽量少进行比较。
tip: 一个比较函数可以返回任何数字
实际上,一个比较函数只需要返回一个正数来表示“更大”和一个负数来表示“更小”。
这样就可以写更短的函数:
let arr = [ 1, 2, 15 ];
arr.sort(function(a, b) { return a - b; });
alert(arr); // 1, 2, 15
tip: 最好用箭头函数
还记得没有找到“函数表达式”的文章吗?我们可以在这里用它们来进行排序。
arr.sort( (a, b) => a - b );
这和上面的另一个更长的版本是完全一样的。
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}.` ); // A message to Bilbo (and other names)
}
split方法有一个可选的第二个数值参数——数组长度的限制。如果它被提供,那么额外的元素将被忽略。在实践中,它很少被使用:
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
alert(arr); // Bilbo, Gandalf
tip: 分成字母
调用 split(s) 方法时,如果 s 是空字符串,那么会将字符串分割成一系列的字母:
let str = "test";
alert( str.split('') ); // t,e,s,t
arr.join(str) 方法相当于 split 方法的逆向操作。它将数组 arr 中的元素按照指定的连接符连接成一个字符串。
例如:
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';');
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
当我们需要迭代一个数组时——我们可以使用forEach。
当我们需要迭代并返回每个元素的数据时——我们可以使用 map。
arr.reduce 和 arr.reduceRight 方法也属于这类操作,不过更复杂一些。它们被用来根据数组计算单个值。
语法是:
let value = arr.reduce(function(previousValue, item, index, arr) {
// ...
}, initial);
这个函数应用于元素。你可能会注意到那些熟悉的论点,从第二点开始:
- item 当前的迭代对象。
- index 当前迭代对象的索引位置。
- arr 被迭代数组。
到目前为止,像 forEach/map,但它还多一个参数:
- previousValue 这是调用之前函数返回的值,第一次调用时等于 initial。
最简单的方法就是举个例子。
我们使用一行代码来求数组元素的总和:
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
这里我们使用了最常见的减少变量,它只使用两个参数。
让我们看看发生了什么。
- 第一次执行的时候,sum 等于初始值(reduce 的最后一个参数)等于 0,current 是数组的第一个元素,也就是 1,因此结果是 1。
- 第二次运行时,sum = 1,让它与数组的第二个参数(2)相加,然后返回。
- 第三运行时,sum = 3,让它与数组的第三个元素(3)相加,然后返回……
计算流如下:
或者以表格的形式,每一行代表的是下一个数组元素的函数调用:
sum | current | result | |
---|---|---|---|
第一次调用 | 0 | 1 | 1 |
第二次调用 | 1 | 2 | 3 |
第三次调用 | 3 | 3 | 6 |
第四次调用 | 6 | 4 | 10 |
第五次调用 | 10 | 5 | 15 |
正如我们所看到的,前一个调用的结果成为下一个调用的第一个参数。
我们还可以省略初始值:
let arr = [1, 2, 3, 4, 5];
// removed initial value from reduce (no 0)
let result = arr.reduce((sum, current) => sum + current);
alert( result ); // 15
结果是一样的。那是因为如果没有初始值,那么 reduce 就会把数组的第一个元素作为初始值,并从第二个元素开始迭代。
计算表与上面相同,减去第一行。
但这样的使用需要特别注意。如果数组是空的,那么在没有初始值的情况下调用 reduce 会产生错误。
let arr = [];
// Error: Reduce of empty array with no initial value
// if the initial value existed, reduce would return it for the empty arr.
arr.reduce((sum, current) => sum + current);
所以建议总是指定初始值。
arr.reduceRight 方法与此类似,不过是从右往左执行的。
迭代:forEach
arr.forEach 方法允许针对数组的每个元素执行特定的回调函数。
语法:
arr.forEach(function(item, index, array) {
// ... do something with item
});
例如,这里展示了数组里的每个元素:
// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
这段代码更详细地描述了他们在目标数组中的位置:
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
函数的结果(如果它返回any)就会被丢弃并被忽略。
Array.isArray
数组不会形成单独的语言类型。它们是基于对象的。
typeof 运算符并不能帮助区分普通对象和数组:
alert(typeof {}); // object
alert(typeof []); // object
但是数组经常被用到,所以有一个专为判断数组的方法:Array.isArray(value),如果 value 是数组的话,就返回 true,否则返回 false。
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
许多方法是支持“thisArg”参数的
几乎所有的数组方法,像 find,filter,map,但是排除里外 sort,接收可选参数 thisArg。
这个参数在上面的章节中没有解释,因为它很少被使用。但为了完整性,我们必须覆盖它。
这是这些方法的完整语法:
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument
参数 thisArg 的值称为了 func 中的 this。
例如,在这里我们使用对象方法作为过滤器,thisArg 用起来就很方便:
let user = {
age: 18,
younger(otherUser) {
return otherUser.age < this.age;
}
};
let users = [
{age: 12},
{age: 16},
{age: 32}
];
// find all users younger than user
let youngerUsers = users.filter(user.younger, user);
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。
- arr.fill(value, start, end) :从索引 start 到 end,重复 value 值填充数组。
- arr.copyWithin(target, start, end) :从位置 target 处,用从数组位置 start 到位置 end 复制元素的开始替换(或者覆盖现有的)。
查看完整列表,请参考文档手册。
乍一看,似乎有这么多的方法,很难记住。但实际上这比看起来要容易得多。
仔细检查一下这张列表,只是为了了解它们。然后解决这一章的任务,这样你就有了数组方法的经验。
然后,当你需要用数组做某件事的时候,你不知道怎么做——来这里,看一下备忘单,找到正确的方法。示例将帮助您正确地编写它。很快你就会自动记住这些方法,而不需要你的具体努力。
扩展阅读
(完)