数组提供了很多方法。为了方便起见,在本章中,它们被分成了几组。
添加/移除项
我们已经知道了从开头或结尾添加和移除项的方法
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
删除一个值。它就是这么做的。对于对象来说很好。但对于数组,我们通常希望其余元素移动并占据释放的空间。我们希望现在有一个更短的数组。
所以,应该使用特殊方法。
arr.splice 方法是数组的瑞士军刀。它可以做所有事情:插入、删除和替换元素。
语法是
arr.splice(start[, deleteCount, elem1, ..., elemN])
它从索引 start
开始修改 arr
:删除 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"
在此和其他数组方法中,允许使用负索引。它们指定从数组末尾开始的位置,如下所示
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
)。start
和 end
都可以为负数,在这种情况下,将假定从数组末尾开始的位置。
它类似于字符串方法 str.slice
,但它不是子字符串,而是子数组。
例如
let arr = ["t", "e", "s", "t"];
alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)
alert( arr.slice(-2) ); // s,t (copy from -2 till the end)
我们也可以不带参数调用它:arr.slice()
创建 arr
的副本。这通常用于获取副本,以便进行不会影响原始数组的进一步转换。
concat
方法 arr.concat 创建一个新数组,其中包含来自其他数组和附加项的值。
语法是
arr.concat(arg1, arg2...)
它接受任意数量的参数——数组或值。
结果是一个新数组,其中包含来自 arr
、arg1
、arg2
等的项。
如果参数 argN
是一个数组,那么它的所有元素都将被复制。否则,将复制参数本身。
例如
let arr = [1, 2];
// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4
// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
// create an array from: arr and [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]
…但如果类数组对象具有特殊的 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 允许为数组的每个元素运行一个函数。
语法
arr.forEach(function(item, index, array) {
// ... do something with an 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}`);
});
函数的结果(如果它返回任何结果)将被丢弃并忽略。
在数组中搜索
现在让我们介绍在数组中搜索的方法。
indexOf/lastIndexOf 和 includes
方法 arr.indexOf 和 arr.includes 具有相似的语法,并且与它们对应的字符串方法执行本质上相同的操作,但它们针对的是项而不是字符
arr.indexOf(item, from)
– 从索引from
开始查找item
,并返回找到它的索引,否则返回-1
。arr.includes(item, from)
– 从索引from
开始查找item
,如果找到,则返回true
。
通常,这些方法仅与一个参数一起使用:要搜索的 item
。默认情况下,搜索是从开头开始的。
例如
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
请注意,indexOf
使用严格相等 ===
进行比较。因此,如果我们查找 false
,它会准确找到 false
,而不是零。
如果我们希望检查 item
是否存在于数组中,并且不需要索引,那么首选 arr.includes
。
方法 arr.lastIndexOf 与 indexOf
相同,但从右向左查找。
let fruits = ['Apple', 'Orange', 'Apple']
alert( fruits.indexOf('Apple') ); // 0 (first Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple)
includes
方法正确处理 NaN
includes
的一个次要但值得注意的特性是,它与 indexOf
不同,可以正确处理 NaN
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0)
alert( arr.includes(NaN) );// true (correct)
这是因为 includes
稍后才添加到 JavaScript 中,并在内部使用更先进的比较算法。
find 和 findIndex/findLastIndex
想象一下,我们有一个对象数组。我们如何找到具有特定条件的对象?
这里 arr.find(fn) 方法派上用场。
语法是
let result = arr.find(function(item, index, array) {
// if true is returned, item is returned and iteration is stopped
// for falsy scenario returns undefined
});
该函数被依次调用以处理数组的元素。
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
方法非常有用。
请注意,在示例中,我们向 find
提供了一个参数为 item => item.id == 1
的函数。这是典型的,此函数的其他参数很少使用。
方法 arr.findIndex 具有相同的语法,但返回找到元素的索引,而不是元素本身。如果什么也没找到,则返回 -1
的值。
方法 arr.findLastIndex 与 findIndex
类似,但从右向左搜索,类似于 lastIndexOf
。
这里有一个示例
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"},
{id: 4, name: "John"}
];
// Find the index of the first John
alert(users.findIndex(user => user.name == 'John')); // 0
// Find the index of the last John
alert(users.findLastIndex(user => user.name == 'John')); // 3
filter
find
方法查找使函数返回 true
的单个(第一个)元素。
如果可能有很多,我们可以使用 arr.filter(fn)。
语法与 find
类似,但 filter
返回一个包含所有匹配元素的数组
let results = arr.filter(function(item, index, array) {
// if true item is pushed to results and the iteration continues
// returns empty array if nothing found
});
例如
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() 的调用对数组进行就地排序,更改其元素顺序。
它还返回已排序的数组,但通常会忽略返回值,因为 arr
本身已修改。
例如
let arr = [ 1, 2, 15 ];
// the method reorders the content of arr
arr.sort();
alert( arr ); // 1, 15, 2
您是否注意到结果中的任何异常情况?
顺序变为 1, 15, 2
。不正确。但为什么?
默认情况下,将项目作为字符串进行排序。
从字面上讲,所有元素都转换为字符串以进行比较。对于字符串,应用词典顺序,并且确实 "2" > "15"
。
要使用我们自己的排序顺序,我们需要将函数作为 arr.sort()
的参数提供。
该函数应比较两个任意值并返回
function compare(a, b) {
if (a > b) return 1; // if the first value is greater than the second
if (a == b) return 0; // if values are equal
if (a < b) return -1; // if the first value is less than the second
}
例如,按数字排序
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
可以是任何内容的数组,对吧?它可能包含数字、字符串、对象或其他任何内容。我们有一组某些项目。要对其进行排序,我们需要一个排序函数,该函数知道如何比较其元素。默认值为字符串顺序。
arr.sort(fn)
方法实现了一个通用排序算法。我们不必关心其内部工作方式(大多数情况下是经过优化的 快速排序 或 Timsort)。它将遍历数组,使用所提供的函数比较其元素并重新排序它们,我们所需要做的就是提供执行比较的 fn
。
顺便说一下,如果我们想知道比较了哪些元素,没有什么可以阻止我们发出警报
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( a + " <> " + b );
return a - b;
});
该算法可能会在过程中将一个元素与多个其他元素进行比较,但它会尝试进行尽可能少的比较。
实际上,比较函数只需要返回一个正数来说“更大”,返回一个负数来说“更小”。
这允许编写更短的函数
let arr = [ 1, 2, 15 ];
arr.sort(function(a, b) { return a - b; });
alert(arr); // 1, 2, 15
localeCompare
还记得 字符串 比较算法吗?它默认按字母的代码比较字母。
对于许多字母表,最好使用 str.localeCompare
方法来正确排序字母,例如 Ö
。
例如,让我们用德语对几个国家进行排序
let countries = ['Österreich', 'Andorra', 'Vietnam'];
alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong)
alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!)
反转
方法 arr.reverse 反转 arr
中元素的顺序。
例如
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
它还返回反转后的数组 arr
。
拆分和连接
以下是现实生活中的情况。我们在编写一个消息传递应用程序,而该人输入了以逗号分隔的收件人列表: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
使用空 s
调用 split(s)
会将字符串拆分为一个字母数组
let str = "test";
alert( str.split('') ); // t,e,s,t
调用 arr.join(glue) 对 split
执行相反的操作。它创建一个由 arr
项组成的字符串,它们之间用 glue
连接。
例如
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // glue the array into a string using ;
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
当我们需要遍历一个数组时 - 我们可以使用 forEach
、for
或 for..of
。
当我们需要遍历并返回每个元素的数据时 - 我们可以使用 map
。
方法 arr.reduce 和 arr.reduceRight 也属于该类型,但更复杂一些。它们用于基于数组计算单个值。
语法是
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
该函数依次应用于所有数组元素,并将其结果“传递”给下一次调用。
参数
accumulator
– 是上一个函数调用的结果,第一次等于initial
(如果提供了initial
)。item
– 是当前数组项。index
– 是它的位置。array
– 是数组。
随着函数的应用,上一个函数调用的结果作为第一个参数传递给下一个函数。
因此,第一个参数本质上是存储所有先前执行的组合结果的累加器。最后,它成为 reduce
的结果。
听起来很复杂?
最简单的理解方法是通过示例。
这里我们在一行中获得数组的总和
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
传递给 reduce
的函数仅使用 2 个参数,这通常就足够了。
让我们看看正在发生的事情的详细信息。
- 在第一次运行时,
sum
是initial
值(reduce
的最后一个参数),等于0
,current
是第一个数组元素,等于1
。因此,函数结果为1
。 - 在第二次运行时,
sum = 1
,我们向其添加第二个数组元素(2
)并返回。 - 在第 3 次运行时,
sum = 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 执行相同操作,但从右到左。
Array.isArray
数组不构成单独的语言类型。它们基于对象。
因此,typeof
无法帮助区分普通对象和数组
alert(typeof {}); // object
alert(typeof []); // object (same)
…但数组使用得如此频繁,因此有专门的方法: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
。
例如,这里我们使用 army
对象的方法作为过滤器,而 thisArg
传递上下文
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
// find users, for who army.canJoin returns true
let soldiers = users.filter(army.canJoin, army);
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
如果在上面的示例中,我们使用 users.filter(army.canJoin)
,则 army.canJoin
将作为独立函数调用,其中 this=undefined
,从而导致即时错误。
可以将对 users.filter(army.canJoin, army)
的调用替换为 users.filter(user => army.canJoin(user))
,它执行相同操作。后者使用得更频繁,因为大多数人更容易理解它。
总结
数组方法备忘单
-
添加/删除元素
push(...items)
– 将项目添加到末尾,pop()
– 从末尾提取一个项目,shift()
– 从开头提取一个项目,unshift(...items)
– 将项目添加到开头。splice(pos, deleteCount, ...items)
– 在索引pos
处删除deleteCount
个元素并插入items
。slice(start, end)
– 创建一个新数组,将从索引start
到end
(不包括)的元素复制到其中。concat(...items)
– 返回一个新数组:复制当前数组的所有成员并向其中添加items
。如果任何items
是一个数组,则取其元素。
-
搜索元素
indexOf/lastIndexOf(item, pos)
– 从位置pos
开始查找item
,如果未找到,则返回索引或-1
。includes(value)
– 如果数组包含value
,则返回true
,否则返回false
。find/filter(func)
– 通过函数筛选元素,返回使函数返回true
的第一个/所有值。findIndex
与find
类似,但返回索引而不是值。
-
遍历元素
forEach(func)
– 对每个元素调用func
,不返回任何内容。
-
转换数组
map(func)
– 根据对每个元素调用func
的结果创建一个新数组。sort(func)
– 就地对数组进行排序,然后返回数组。reverse()
– 就地反转数组,然后返回数组。split/join
– 将字符串转换为数组,反之亦然。reduce/reduceRight(func, initial)
– 通过对每个元素调用func
并传递调用之间的中间结果,计算数组上的单个值。
-
此外
Array.isArray(value)
检查value
是否为数组,如果是,则返回true
,否则返回false
。
请注意,方法 sort
、reverse
和 splice
会修改数组本身。
这些方法是最常用的方法,它们涵盖了 99% 的用例。但还有其他一些方法
-
arr.some(fn)/arr.every(fn) 检查数组。
函数
fn
在数组的每个元素上调用,类似于map
。如果任何/所有结果为true
,则返回true
,否则返回false
。这些方法的行为类似于
||
和&&
运算符:如果fn
返回真值,则arr.some()
立即返回true
并停止迭代其余项目;如果fn
返回假值,则arr.every()
立即返回false
并停止迭代其余项目。我们可以使用
every
来比较数组function arraysEqual(arr1, arr2) { return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]); } alert( arraysEqual([1, 2], [1, 2])); // true
-
arr.fill(value, start, end) – 使用重复的
value
从索引start
到end
填充数组。 -
arr.copyWithin(target, start, end) – 将其元素从位置
start
复制到位置end
,到其自身,在位置target
(覆盖现有元素)。 -
arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组。
有关完整列表,请参见手册。
乍一看,似乎有很多方法,很难记住。但实际上,这要容易得多。
浏览备忘单,以便了解它们。然后解决本章中的任务进行练习,以便获得使用数组方法的经验。
此后,无论何时需要对数组执行某些操作,但不知道如何执行时,请到这里,查看备忘单并找到正确的方法。示例将帮助您正确编写它。很快,您将自动记住这些方法,而无需您付出特别的努力。
评论
<code>
标签,对于多行代码 – 将其包装在<pre>
标签中,对于超过 10 行的代码 – 使用沙箱 (plnkr、jsbin、codepen…)