目录
- 7.1 创建数组
- 7.2 数组元素的读和写
- 7.3 稀疏数组
- 7.4 数组长度
- 7.5 添加和删除数组元素
- 7.6 迭代数组
- 7.7 多维数组
- 7.8 数组方法
- 7.9 类数组对象
- 7.10 字符串作为数组
- 7.11 总结
本章介绍了数组,它是JavaScript和大多数其他编程语言中的基本数据类型。数组是值的有序集合。每个值称为一个元素,每个元素在数组中都有一个数字位置,称为其索引。JavaScript 数组是非类型化的:数组元素可以是任何类型的,同一数组的不同元素可以是不同的类型。数组元素甚至可以是对象或其他数组,这允许您创建复杂的数据结构,例如对象数组和数组数组。JavaScript 数组从零开始,使用32位整型索引:第一个元素的索引为 0,最大可能的索引为4294967294(232−2),最大数组大小为4294967295个元素。JavaScript 数组是动态的:它们根据需要增长或缩小,并且在创建数组时不需要为数组声明固定大小,也不需要在大小更改时重新分配数组。JavaScript 数组可能是稀疏的:元素不需要有连续的索引,并且可能存在间隙。每个JavaScript 数组都有一个 length 属性。对于非稀疏数组,此属性指定数组中的元素个数。对于稀疏数组,length 大于任何元素的最大索引。
JavaScript 数组是 JavaScript 对象的一种特殊形式,数组索引实际上只不过是碰巧是整数的属性名。我们将在本章的其他地方详细讨论数组的特殊性。实现通常会优化数组,以便访问数字索引数组元素通常比访问常规对象属性快得多。
数组从 Array.prototype 继承属性,它定义了一套丰富的数组操作方法,如§7.8所述。这些方法大多是通用的,这意味着它们不仅适用于真正的数组,而且适用于任何“类数组对象”。最后,JavaScript字符串的行为类似于字符数组,我们将在§7.10中对此进行讨论。
ES6 引入了一组新的数组类,统称为“类型化数组”。与常规 JavaScript 数组不同,类型化数组具有固定长度和固定的数值元素类型。它们提供对二进制数据的高性能和字节级访问,这将在§11.2中进行介绍。
7.1 创建数组
有几种方法可以创建数组。下面的小节解释了如何使用以下方法创建数组:
- 数组字面量
- 在一个可迭代对象上使用 … 展开运算符
- Array() 构造函数
- Array.of() 和 Array.from() 工厂方法
7.1.1 数组字面量
到目前为止,创建数组的最简单方法是使用数组字面量,它只是一个方括号内以逗号分隔的数组元素列表。例如:
let empty = []; // 没有元素的数组
let primes = [2, 3, 5, 7, 11]; // 包含5个数字元素的数组
let misc = [ 1.1, true, "a", ]; // 3个不同类型的元素+尾随逗号
数组字面量中的值不必是常量;它们可以是任意表达式:
let base = 1024;
let table = [base, base+1, base+2, base+3];
数组字面量可以包含对象字面量或其他数组字面量:
let b = [[1, {x: 1, y: 2}], [2, {x: 3, y: 4}]];
如果一个数组字面量在一行中包含多个逗号,且这两个逗号之间没有值,则该数组是稀疏的(见§7.3)。忽略其值的数组元素不存在,但如果查询这些元素,则返回 undefined:
let count = [1,,3]; // 索引0和2处的元素。索引1处没有元素
let undefs = [,,]; // 没有元素但长度为2的数组
数组字面量语法允许可选的尾随逗号,因此[,,]的长度为2,而不是3。
7.1.2 展开运算符
在 ES6 及更高版本中,您可以使用“展开运算符”…,在数组字面量中包含一个数组的元素:
let a = [1, 2, 3];
let b = [0, ...a, 4]; // b == [0, 1, 2, 3, 4]
三个点“展开”数组 a,使其元素成为正在创建的数组字面量中的元素。这就好像 …a 被数组 a 的元素替换了,数组 a 是包含数组字面量的一部分。(请注意,尽管我们将这三个点称为展开运算符,但这不是一个真正的运算符,因为它只能用于数组字面量,以及我们在本书后面将看到的函数调用中。)
展开操作符是创建数组(浅)拷贝的一种简便方法:
let original = [1,2,3];
let copy = [...original];
copy[0] = 0; // 修改拷贝不会更改原始数组
original[0] // => 1
“展开”操作符适用于任何可迭代对象。(可迭代对象是 for/of 循环迭代的对象;我们第一次在§5.4.4中看到了它们,我们将在第12章中看到更多关于它们的内容。)字符串是可迭代的,因此您可以使用展开运算符将任何字符串转换为单字符串数组:
let digits = [..."0123456789ABCDEF"];
digits // => ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
集合对象(§11.1.1)是可迭代的,因此从数组中删除重复元素的一个简单方法是将数组转换为集合,然后使用展开运算符立即将集合转换回数组:
let letters = [..."hello world"];
[...new Set(letters)] // => ["h","e","l","o"," ","w","r","d"]
7.1.3 Array() 构造函数
另一种创建数组的方法是使用 Array() 构造函数。可以用三种不同的方式调用此构造函数:
- 无参调用:
let a = new Array();
此方法创建一个没有元素的空数组,它与数组字面量[]等效。
- 用一个指定长度的数字参数调用它:
let a = new Array(10);
此技术创建具有指定长度的数组。当您预先知道需要多少元素时,可以使用Array()构造函数的这种形式预先分配数组。注意,数组中没有存储任何值,没有数组索引属性“0”、“1”,甚至没有定义数组等。
- 为数组显式指定两个或多个数组元素或单个非数字元素:
let a = new Array(5, 4, 3, 2, 1, "testing, testing");
在这种形式下,构造函数参数成为新数组的元素。使用数组字面量几乎总是比 Array() 构造函数的用法更简单。
7.1.4 Array.of()
当使用一个数值参数调用 Array() 构造函数时,它将该参数用作数组长度。但当使用多个数值参数调用时,它将这些参数视为要创建的数组的元素。这意味着 Array() 构造函数不能用于创建具有单个数值元素的数组。
在ES6中 Array.of() 函数解决了这个问题:它是一个工厂方法,它创建并返回一个新数组,使用它的参数值(不管有多少个)作为数组元素:
Array.of() // => []; 返回不带参数的空数组
Array.of(10) // => [10]; 可以使用单个数字参数创建数组
Array.of(1,2,3) // => [1, 2, 3]
7.1.5 Array.from()
Array.from 是 ES6 中引入的另一种数组工厂方法。它期望可迭代或类数组的对象作为其第一个参数,并返回一个包含该对象元素的新数组。传入一个可迭代参数,Array.from(iterable)的工作原理与展开运算符 […iterable] 相同。这也是制作数组拷贝的一种简单方法:
let copy = Array.from(original);
Array.from() 也很重要,因为它定义了一种生成类数组对象的真正数组副本的方法。类数组的对象是非数组对象,这些对象具有数值长度属性,其值与名称恰好为整数的属性一起存储。在使用客户端 JavaScript 时,某些 web 浏览器方法的返回值类似于数组,如果首先将它们转换为真正的数组,则使用它们会更容易:
let truearray = Array.from(arraylike);
Array.from() 还接受可选的第二个参数。如果将函数作为第二个参数传递,则在生成新数组时,源对象中的每个元素都将传递给指定的函数,函数的返回值将存储在数组中,而不是原始值。(这与本章后面将介绍的数组的 map() 方法非常相似,但是在构建数组时执行映射比构建数组然后将其映射到另一个新数组更有效。)
7.2 数组元素的读和写
使用 [] 运算符访问数组的元素。对数组的引用应该出现在括号的左边。具有非负整数值的任意表达式应位于括号内。您可以使用此语法来读取和写入数组元素的值。因此,以下都是合法的 JavaScript 语句:
let a = ["world"]; // 从一个元素数组开始
let value = a[0]; // 读第0个元素
a[1] = 3.14; // 写第1个元素
let i = 2;
a[i] = 3; // 写第2个元素
a[i + 1] = "hello"; // 写入位置3的元素
a[a[i]] = a[0]; // 读第0个和第2个元素,写第3个元素
数组的特殊之处在于,当使用小于232–1的非负整数的属性名时,数组会自动为您维护 length 属性的值。例如,在前面的例子中,我们用一个元素创建了一个数组 a。然后我们在索引 1、2 和 3 处赋值。数组的 length 属性根据我们的操作发生了改变,因此:
a.length // => 4
记住数组是一种特殊的对象。用于访问数组元素的方括号与用于访问对象属性的方括号的工作原理相同。JavaScript 将指定的数字数组索引转换为字符串–索引 1 变成字符串“1”-- 然后将该字符串用作属性名。将索引从数字转换为字符串没有什么特别之处:您也可以使用常规对象进行转换:
let o = {}; // 创建普通对象
o[1] = "one"; // 用整数索引它
o["1"] // => "one"; 数字和字符串属性名称相同
明确区分数组索引和对象属性名是很有帮助的。所有索引都是属性名,但只有 0 到 232–2 之间整数的属性名才是索引。所有数组都是对象,您可以在它们上创建任何名称的属性。但是,如果使用的属性是数组索引,那么数组会根据需要更新其 length 属性。
请注意,您可以使用负数或非整数的数字为数组编制索引。执行此操作时,数字将转换为字符串,并将该字符串用作属性名称。由于名称不是非负整数,因此它被视为常规对象属性,而不是数组索引。另外,如果用一个恰好是非负整数的字符串为数组编制索引,则它的行为类似于数组索引,而不是对象属性。如果使用与整数相同的浮点数,则同样适用:
a[-1.23] = true; // 这将创建一个名为“-1.23”的属性
a["1000"] = 0; // 这是数组的第1001个元素
a[1.000] = 1; // 数组索引1。与a[1]=1相同;
数组索引只是一种特殊类型的对象属性名,这意味着 JavaScript 数组没有“越界”错误的概念。当您试图查询任何对象的不存在属性时,不会得到错误;你只是得到一个 undefined 值。对于数组和对象也是如此:
let a = [true, false]; // 此数组在索引0和1处有元素
a[2] // => undefined; 此索引中没有元素。
a[-1] // => undefined; 没有此名称的属性。
7.3 稀疏数组
稀疏数组是指元素没有从 0 开始的连续索引的数组。通常,数组的length属性指定数组中的元素数。如果数组是稀疏的,则 length 属性的值大于元素数。稀疏数组可以使用Array() 构造函数创建,也可以简单地通过给给大于当前数组长度的数组索引赋值来创建。
let a = new Array(5); // 没有元素,但是a.length是5。
a = []; // 创建一个没有元素且长度为0的数组。
a[1000] = 0; // 赋值添加一个元素,但将长度设置为1001。
稍后我们将看到,您还可以使用 delete 操作符使数组稀疏。
足够稀疏的数组通常以比密集数组更慢、更节省内存的方式实现,并且在这样的数组中查找元素所需的时间与常规对象属性查找的时间相同。
请注意,当您在数组文本中省略一个值时(在[1,3]中使用重复的逗号),得到的数组是稀疏的,并且省略的元素根本不存在:
let a1 = [,]; // 此数组没有元素,长度为1
let a2 = [undefined]; // 此数组有一个 undefined 的元素
0 in a1 // => false: a1没有索引为0的元素
0 in a2 // => true: a2在索引0处具有 undefined 的值
理解稀疏数组是理解 JavaScript 数组真正本质的一个重要部分。然而,在实践中,您将使用的大多数 JavaScript 数组都不是稀疏的。而且,如果必须使用稀疏数组,那么代码可能会像对待具有 undefined 元素的非稀疏数组一样对待它。
7.4 数组长度
每个数组都有一个 length 属性,正是这个属性使数组不同于常规 JavaScript 对象。对于密集(即非稀疏)的数组,length 属性指定数组中的元素数。它的值比数组中的最高索引多一个:
[].length // => 0: 数组没有元素
["a","b","c"].length // => 3: 最高索引为2,长度为3
当一个数组是稀疏的时,length 属性大于元素的个数,我们只能说长度保证大于数组中每个元素的索引。或者,换句话说,数组(稀疏或不稀疏)永远不会有索引大于或等于其长度的元素。为了保持这种不变性,数组有两种特殊的行为。我们上面描述的第一个:如果为大于或等于数组当前长度的索引 i 的元素赋值,length属性的值将设置为 i+1。
数组实现的第二个特殊行为是,如果将 length 属性设置为小于其当前值的非负整数 n,则索引大于或等于 n 的任何数组元素都将从数组中删除:
a = [1,2,3,4,5]; // 从5个元素的数组开始。
a.length = 3; // a 现在是 [1,2,3]。
a.length = 0; // 删除所有元素。a 是 []。
a.length = 5; // 长度是5,但是没有元素,比如 new Array(5)
也可以将数组的 length 属性设置为大于其当前值的值。这样做实际上并不会向数组中添加任何新元素;它只是在数组末尾创建一个稀疏区域。
7.5 添加和删除数组元素
我们已经看到了向数组添加元素的最简单方法:只需为新索引赋值:
let a = []; // 从空数组开始。
a[0] = "zero"; // 并添加元素。
a[1] = "one";
也可以使用 push() 方法将一个或多个值添加到数组的末尾:
let a = []; // 从空数组开始
a.push("zero"); // 在末尾添加值。a = ["zero"]
a.push("one", "two"); // 再加两个值。a = ["zero", "one", "two"]
将值 push 到数组 a 上与将值赋给 [a.length] 相同。您可以使用 unshift() 方法(如§7.8所述)在数组的开头插入一个值,将现有数组元素转移到更高的索引位置。pop() 方法与 push() 相反:它删除数组的最后一个元素并返回它,从而将数组的长度减少1。类似地,shift() 方法删除并返回数组的第一个元素,将长度减少1,并将所有元素下移到比当前索引低一个位置的索引。有关这些方法的更多信息,请参见§7.8。
可以使用 delete 操作符删除数组元素,就像可以删除对象属性一样:
let a = [1,2,3];
delete a[2]; // 现在索引2中没有元素
2 in a // => false: 未定义数组索引2
a.length // => 3: delete 不影响数组长度
删除数组元素类似于(但有点不同)将 undefined 赋值给该元素。请注意,对数组元素使用 delete 不会更改 length 属性,也不会向下移动索引较高的元素以填补被删除属性留下的空白。如果从数组中删除元素,则数组将变为稀疏。
如上所述,您还可以通过将 length 属性设置为新的所需长度,从数组末尾移除元素。
最后,splice() 是插入、删除或替换数组元素的通用方法。它可以根据需要更改length属性并将数组元素移到更高或更低的索引位置。详见§7.8。
7.6 迭代数组
从 ES6 开始,循环遍历数组(或任何可迭代对象)的每个元素的最简单方法是 for/of 循环,§5.4.4:
let letters = [..."Hello world"]; // 字符数组
let string = "";
for (let letter of letters) {
string += letter;
}
string // => "Hello world"; 我们重新组装了原始字符串
for/of 循环使用的内置数组迭代器按升序返回数组的元素。对于稀疏数组,它没有特殊的行为,对于不存在的数组元素,它只返回 undefined。
如果要对数组使用 for/of 循环,并且需要知道每个数组元素的索引,请使用数组的entries() 方法,同时使用解构赋值,如下所示:
let everyother = "";
for (let [index, letter] of letters.entries()) {
if (index % 2 === 0) everyother += letter; // 偶数索引的字母
}
everyother // => "Hlowrd"
迭代数组的另一个好方法是使用 forEach() 。这不是 for 循环的一种新形式,而是一种数组方法,它为数组迭代提供了一种函数方法。将函数传递给数组的 forEach() 方法,forEach() 对数组的每个元素调用一次函数:
let uppercase = "";
letters.forEach(letter => { // Note arrow function syntax here
uppercase += letter.toUpperCase();
});
uppercase // => "HELLO WORLD"
正如您所期望的那样,forEach() 按顺序迭代数组,它实际上将数组索引作为第二个参数传递给函数,这有时很有用。与 for/of 循环不同,forEach() 可以识别稀疏数组,并且不会为不存在的元素调用函数。
§7.8.1更详细地记录了 forEach() 方法。本节还介绍了相关的方法,如 map() 和 filter() 执行特殊类型的数组迭代。
您还可以使用一个老式的 for 循环(§5.4.3)遍历数组的元素:
let vowels = "";
for (let i = 0; i < letters.length; i++) { // 对于数组中的每个索引
let letter = letters[i]; // 获取索引处的元素
if (/[aeiou]/.test(letter)) { // 使用正则表达式测试
vowels += letter; // 如果是元音,记住它
}
}
vowels // => "eoo"
在嵌套循环或其他性能至关重要的上下文中,有时您可能会看到下面编写此基本数组迭代循环时,以便只查找一次数组长度,而不是每次迭代都查找。以下两种 for 循环形式都是惯用的,虽然不是特别常见,但是对于现代 JavaScript 解释器,它们是否会对性能产生任何影响还不清楚:
// 将数组长度保存到局部变量中
for (let i = 0, len = letters.length; i < len; i++) {
// 循环体保持不变
}
// 从数组的结尾向后迭代到开头
for (let i = letters.length - 1; i >= 0; i--) {
// 循环体保持不变
}
这些示例假定数组是密集的,并且所有元素都包含有效的数据。如果不是这样,则应该在使用数组元素之前测试它们。如果要跳过未定义和不存在的元素,可以编写:
for(let i = 0; i < a.length; i++) {
if (a[i] === undefined) continue; // 跳过未定义的+不存在的元素
// 此处循环体
}
7.7 多维数组
JavaScript不支持真正的多维数组,但您可以使用数组的数组来模拟。要访问数组的数组中的值,只需使用 [ ] 操作符两次。例如,假设变量矩阵是一个数字数组的数组。矩阵 [x] 中的每个元素都是一个数字数组。要访问此数组中的特定数字,您将编写矩阵 [x][y]。下面是一个具体的例子,它使用二维数组作为乘法表:
// 创建多维数组
let table = new Array(10); // 表格有10行
for (let i = 0; i < table.length; i++) {
table[i] = new Array(10); // 每行有10列
}
// 初始化数组
for (let row = 0; row < table.length; row++) {
for (let col = 0; col < table[row].length; col++) {
table[row][col] = row * col;
}
}
// 使用多维数组计算5*7
table[5][7] // => 35
7.8 数组方法
前面几节主要讨论处理数组的基本 JavaScript 语法。不过,一般来说,由 Array 类定义的方法是最强大的。下一节将记录这些方法。在阅读这些方法时,请记住,有些方法修改了被调用的数组,有些方法保持数组不变。许多方法返回一个数组:有时,这是一个新数组,而原始数组不变。其他时候,方法会就地修改数组,并且还会返回对修改后数组的引用。
以下各小节包括一组相关数组方法:
- 迭代器方法在数组的元素上循环,通常调用在每个元素上指定的函数。
- 堆栈和队列方法在数组的开头和结尾添加和删除数组元素。
- 子数组方法用于提取、删除、插入、填充和复制较大数组中的连续区域。
- 搜索和排序方法用于查找数组中的元素并对数组中的元素进行排序。
下面的小节还将介绍 Array 类的静态方法以及用于连接数组和将数组转换为字符串的一些其他方法。
7.8.1 数组迭代方法
本节中描述的方法通过将数组元素按顺序传递给您提供的函数来迭代数组,它们提供了迭代、映射、过滤、测试和折叠数组的方便方法。
然而,在我们详细解释这些方法之前,有必要对它们进行一些概括。首先,所有这些方法都接受一个函数作为它们的第一个参数,并为数组的每个元素(或某些元素)调用该函数一次。如果数组是稀疏的,则不为不存在的元素调用传递的函数。在大多数情况下,使用三个参数调用您提供的函数:数组元素的值、数组元素的索引和数组本身。通常,您只需要这些参数值中的第一个,而可以忽略第二个和第三个值。
以下小节中描述的大多数迭代方法都接受可选的第二个参数。如果指定了该函数,则该函数将被调用,就像它是第二个参数的方法一样。也就是说,传递的第二个参数将成为作为第一个参数传递的函数中 this 关键字的值。传递的函数的返回值通常很重要,但是不同的方法以不同的方式处理返回值。这里描述的方法都不会修改调用它们的数组(当然,传递的函数可以修改数组)。
这些函数中的每一个都是用一个函数作为其第一个参数来调用的,通常将该函数内联定义为方法调用表达式的一部分,而不是使用在别处定义的现有函数。箭头函数语法(见§8.1.3)与这些方法配合得特别好,我们将在下面的示例中使用它。
forEach()
forEach() 方法遍历数组,调用为每个元素指定的函数。如前所述,将函数作为第一个参数传递给 forEach()。forEach() 然后用三个参数调用函数:数组元素的值、数组元素的索引和数组本身。如果只关心数组元素的值,则可以只使用一个参数编写函数-其他参数将被忽略:
let data = [1,2,3,4,5], sum = 0;
// 计算数组元素的和
data.forEach(value => { sum += value; }); // sum == 15
// 现在递增每个数组元素
data.forEach(function(v, i, a) { a[i] = v + 1; }); // data == [2,3,4,5,6]
请注意,forEach() 只能遍历所有的元素,没有办法提前终止迭代。也就是说,没有等效的在常规的 for 循环中的 break 语句。
map()
map() 方法将调用它的数组的每个元素传递给指定的函数,并返回一个包含函数返回值的数组。例如:
let a = [1, 2, 3];
a.map(x => x*x) // => [1, 4, 9]: 函数接受输入x并返回x*x
传递给 map() 的函数与传递给 forEach() 的函数调用方式相同。但是,对于 map() 方法,传递的函数应该返回一个值。注意,map() 返回一个新数组:它不修改调用它的数组。如果该数组是稀疏的,则不会为缺少的元素调用函数,但返回的数组将以与原始数组相同的方式稀疏:它将具有相同的长度和相同的缺失元素。
filter()
filter() 方法返回一个数组,其中包含调用它的数组元素的子集。传递给它的函数应该是是个谓词函数:返回 true 或 false 的函数。与 forEach() 和 map() 一样调用谓词。如果返回值为 true 或转换为 true 的值,则传递给谓词的元素是子集的成员,并被添加到将成为返回值的数组中。示例:
let a = [5, 4, 3, 2, 1];
a.filter(x => x < 3) // => [2, 1]; 值小于3
a.filter((x,i) => i%2 === 0) // => [5, 3, 1]; 索引位置是偶数的元素
注意 filter() 会跳过稀疏数组中缺少的元素,并且它的返回值总是密集的。要缩小稀疏阵列中的间距,可以执行以下操作:
let dense = sparse.filter(() => true);
要缩小间距并删除未定义和空元素,可以使用 filter,如下所示:
a = a.filter(x => x !== undefined && x !== null);
译者注
:filter只跳过缺失元素,但是不跳过 undefined 和 null 元素,如下:let a = [1, 2, , null,3]; console.log(a); // [ 1, 2, <1 empty item>, null, 3 ] console.log(a.filter(() => true)); // [ 1, 2, null, 3 ] console.log(a.filter(x => x !== undefined && x !== null)); // [ 1, 2, 3 ]
find() 和 findIndex()
find() 和 findIndex() 方法类似于 filter(),因为它们会进行迭代数组查找谓词函数返回其真值的元素。但是,与 filter() 不同,这两个方法在谓词第一次找到元素时停止迭代。发生这种情况时,find() 返回匹配元素,findIndex() 返回匹配元素的索引。如果没有匹配
元素,find() 返回 undefined,findIndex() 返回 -1:
let a = [1,2,3,4,5];
a.findIndex(x => x === 3) // => 2; 值3出现在索引2处
a.findIndex(x => x < 0) // => -1; 数组中没有负数
a.find(x => x % 5 === 0) // => 5:这是5的倍数
a.find(x => x % 7 === 0) // => undefined: 数组中没有7的倍数
every() 和 some()
every() 和 some() 方法是数组谓词:它们将指定的谓词函数应用于数组的元素,然后返回 true 或 false。
every() 方法类似于数学上的“全部”量词 ∀:当且仅当谓词函数对数组中的所有元素返回 true 时,它才会返回 true:
let a = [1,2,3,4,5];
a.every(x => x < 10) // => true: 所有元素 < 10.
a.every(x => x % 2 === 0) // => false: 并非所有元素都是偶数
some() 方法类似于数学上的“存在”量词 ∃:如果数组中至少存在一个元素,谓词返回 true,则返回 true;如果当且仅当谓词对数组的所有元素返回 false 时,则返回 false:
let a = [1,2,3,4,5];
a.some(x => x%2===0) // => true; a有一些偶数。
a.some(isNaN) // => false; a没有非数字。
请注意,every() 和 some() 一旦知道要返回什么值,就会停止迭代数组元素。some() 在谓词第一次返回 true 时返回 true,并且只有在谓词始终返回 false 时才迭代整个数组。every() 则相反:当谓词第一次返回 fals e时,它返回 false;如果谓词始终返回 true,则只迭代所有元素。还要注意,根据数学约定,every() 在空数组上调用时返回 true,some 返回 false。
reduce() 和 reduceRight()
reduce() 和 reduceRight() 方法使用指定的函数组合数组元素,以生成单个值。这是函数式编程中常见的操作,也称为“注入”和“折叠”。示例有助于说明其工作原理:
let a = [1,2,3,4,5];
a.reduce((x,y) => x+y, 0) // => 15; 值的总和
a.reduce((x,y) => x*y, 1) // => 120; 值的连乘积
a.reduce((x,y) => (x > y) ? x : y) // => 5; 最大值
reduce() 接受两个参数。第一个是执行折叠操作的函数。这个折叠函数的任务是以某种方式将两个值合并或折叠为一个值,并返回该折叠后的值。在我们这里展示的示例中,函数通过将两个值相加、相乘和选择最大值来组合这两个值。第二个(可选)参数是传递给函数的初始值。
reduce() 使用的函数与 forEach() 和 map() 使用的函数不同。熟悉的值、索引和数组值作为第二、第三和第四个参数传递。第一个参数是到目前为止折叠的累积结果。在第一次调用函数时,第一个参数是作为第二个参数传递给 reduce()的初始值。在后续调用中,它是上一次函数调用返回的值。在第一个示例中,首先使用参数 0 和 1 调用折叠函数。它将这些相加并返回 1。然后用参数 1 和 2 再次调用它并返回 3。接下来,计算3+3=6,然后 6+4=10,最后 10+5=15。最终值 15 将成为 reduce() 的返回值。
您可能已经注意到,本例中对 reduce() 的第三次调用只有一个参数:没有指定初始值。当您像这样在没有初始值的情况下调用 reduce() 时,它使用数组的第一个元素作为初始值。这意味着对折叠函数的第一次调用将使用第一个和第二个数组元素作为第一个和第二个参数。在计算累加和与乘积示例中,我们可以省略初始值参数。
在没有初始值参数的空数组上调用 reduce() 会导致 TypeError。如果只使用一个值调用它 — 要么是只有一个元素的数组和没有初始值,要么是空数组和一个初始值 — 那么它只返回一个值,而不调用折叠函数。
reduceRight() 的工作原理与 reduce() 相同,只是它处理的数组从最高索引到最低索引(从右到左),而不是从最低到最高。如果折叠操作具有从右到左的结合性,则可能需要执行此操作,例如:
// 计算 2^(3^4). 求幂具有从右到左的优先顺序
let a = [2, 3, 4];
a.reduceRight((acc,val) => Math.pow(val,acc)) // => 2.4178516392292583e+24
请注意,reduce() 和 reduceRight() 都没有这样的可选参数,该参数指定调用折叠函数的 this 值。取而代之的是可选的初始值参数。参考 Function.bind() 方法(§8.7.5),如果您需要将折叠函数作为特定对象的方法调用。
为了简单起见,到目前为止显示的示例都是数值的,但是 reduce() 和 reduceRight() 并不是专门用于数学计算的。任何可以将两个值(如两个对象)合并为同一类型的一个值的函数都可以用作折叠函数。另一方面,使用数组折叠表示的算法可能会很快变得复杂且难以理解,并且您可能会发现,如果使用常规的循环结构来处理数组,则更容易阅读、编写和推理代码。
7.8.2 使用 flat() 和 flatMap() 展平数组
在ES2019中,flat() 方法创建并返回一个新数组,该数组包含与调用它的数组相同的元素,但任何本身是数组的元素都会“展平”到返回的数组中。例如:
[1, [2, 3]].flat() // => [1, 2, 3]
[1, [2, [3]]].flat() // => [1, 2, [3]]
当不带参数调用时,flat() 会展平一级嵌套。原始数组中本身是数组的元素被展平,但这些数组的数组元素不会展平。如果要展平更多级别,请将数字传递给 flat():
let a = [1, [2, [3, [4]]]];
a.flat(1) // => [1, 2, [3, [4]]]
a.flat(2) // => [1, 2, 3, [4]]
a.flat(3) // => [1, 2, 3, 4]
a.flat(4) // => [1, 2, 3, 4]
flatMap() 方法的工作原理与 map() 方法相同,只是返回的数组会自动变平,就像传递给 flat() 一样。也就是说,调用 a.flatMap(f) 与 a.map(f).flat() 相同(但效率更高):
let phrases = ["hello world", "the definitive guide"];
let words = phrases.flatMap(phrase => phrase.split(" "));
words // => ["hello", "world", "the", "definitive", "guide"];
可以将 flatMap() 看作 map() 的泛化,它允许输入数组的每个元素映射到输出数组的任意数量的元素。特别是,flatMap() 允许您将输入元素映射到空数组,空数组在输出数组中展平时将自动去掉:
// 将非负数映射到它们的平方根
[-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) // => [1, 1.4142135623730951]
7.8.3 使用 concat() 连接数组
concat() 方法创建并返回一个新数组,该数组包含调用 concat() 的原始数组的元素,后跟 concat() 的每个参数。如果这些参数中的任何一个本身是数组,那么连接的是数组元素,而不是数组本身。但是请注意,concat() 不会递归地展平数组的数组。concat() 不修改调用它的数组:
let a = [1,2,3];
a.concat(4, 5) // => [1,2,3,4,5]
a.concat([4,5],[6,7]) // => [1,2,3,4,5,6,7]; 数组被展平
a.concat(4, [5,[6,7]]) // => [1,2,3,4,5,[6,7]]; 不展平嵌套数组
a // => [1,2,3]; 原始数组未修改
请注意,concat() 将生成调用它的数组的新副本。在许多情况下,这是正确的做法,但这是一个昂贵的操作。如果您发现自己正在编写类似 a=a.concat(x) 的代码,那么应该考虑使用 push() 或 splice() 修改数组,而不是创建新的数组。
7.8.4 栈和队列方法 push, pop(), shift(), unshift()
push() 和 pop() 方法允许您像处理堆栈一样处理数组。push() 方法的作用是:将一个或多个新元素追加到数组的末尾,并返回数组的新长度。与 concat() 不同,push() 不展平数组参数。pop() 方法执行相反的操作:删除数组的最后一个元素,减少数组长度,并返回它删除的值。请注意,这两种方法都会就地修改数组。push() 和 pop() 的组合允许您使用 JavaScript 数组来实现先进先出的堆栈。例如:
let stack = []; // stack == []
stack.push(1,2); // stack == [1,2];
stack.pop(); // stack == [1]; returns 2
stack.push(3); // stack == [1,3]
stack.pop(); // stack == [1]; returns 3
stack.push([4,5]); // stack == [1,[4,5]]
stack.pop() // stack == [1]; returns [4,5]
stack.pop(); // stack == []; returns 1
push() 方法不会展平传递给它的数组,但如果要将所有元素从一个数组追加到另一个数组,可以使用展开运算符(§8.3.4)显式展开它:
a.push(...values);
unshift() 和 shift() 方法的行为与 push() 和 pop() 非常相似,但它们从数组的开头插入和删除元素,而不是从结尾插入和删除元素。unshift() 将单个元素或多个元素添加到数组的开头,将现有数组元素移动到更高的索引以腾出空间,并返回数组的新长度。shift() 删除并返回数组的第一个元素,将所有后续元素向下移动一个位置,以此占用开头删除元素后留下的新空位。可以使用 unshift() 和 shift() 实现堆栈,但它的效率比使用 push() 和 pop() 要低,因为每次在数组开头添加或删除元素时,数组元素都需要上下移动。但是,您可以使用 push() 在数组末尾添加元素,并将其从数组的开头删除,从而实现队列数据结构:
let q = []; // q == []
q.push(1,2); // q == [1,2]
q.shift(); // q == [2]; returns 1
q.push(3) // q == [2, 3]
q.shift() // q == [3]; returns 2
q.shift() // q == []; returns 3
unshift() 有一个特性值得注意,因为您可能会发现它令人惊讶。将多个参数传递给unshift() 时,它们将同时插入,这意味着它们在数组中的顺序与一次插入一个参数时的顺序不同:
let a = []; // a == []
a.unshift(1) // a == [1]
a.unshift(2) // a == [2, 1]
a = []; // a == []
a.unshift(1,2) // a == [1, 2]
7.8.5 子数组方法 slice(), splice(), fill(), copyWithin()
数组定义了许多方法,这些方法在连续区域、子数组或数组的“片段”上工作。以下各节介绍提取、替换、填充和复制切片的方法。
slice()
slice() 方法的作用是:返回指定数组的片段或子数组。它的两个参数指定要返回的片段的开始位置和结束位置。返回的数组包含第一个参数指定的元素和所有后续元素,直到(但不包括)第二个参数指定的元素。如果只指定了一个参数,则返回的数组包含从数组开始位置到数组结尾的所有元素。如果两个参数都是负数,则指定相对于数组长度的数组元素。例如,参数 –1 指定数组中的最后一个元素,参数 –2 指定该元素之前的元素。请注意,slice() 不会修改调用它的数组。以下是一些示例:
let a = [1,2,3,4,5];
a.slice(0,3); // 返回 [1,2,3]
a.slice(3); // 返回 [4,5]
a.slice(1,-1); // 返回 [2,3,4]
a.slice(-3,-2); // 返回 [3]
splice()
splice() 是一种用于从数组中插入或删除元素的通用方法。与 slice() 和 concat() 不同,splice() 修改调用它的数组。请注意,splice() 和 slice() 的名称非常相似,但执行的操作却截然不同。
splice() 可以从数组中删除元素,向数组中插入新元素,或者同时执行这两种操作。插入或删除点之后的数组元素的索引会根据需要增加或减少,以便与数组的其余部分保持连续。splice() 的第一个参数指定开始插入和/或删除的数组位置。第二个参数指定应从数组中删除的元素数。(请注意,这是这两种方法之间的另一个区别。slice() 的第二个参数是结束位置。splice() 的第二个参数是一个长度。)如果省略第二个参数,则将删除从开始元素到数组结尾的所有数组元素。splice() 返回已删除元素的数组,如果未删除元素,则返回空数组。例如:
let a = [1,2,3,4,5,6,7,8];
a.splice(4) // => [5,6,7,8]; a 现在是 [1,2,3,4]
a.splice(1,2) // => [2,3]; a 现在是 [1,4]
a.splice(1,1) // => [4]; a 现在是 [1]
splice() 的前两个参数指定要删除的数组元素。这些参数后面可以跟任意数量的附加参数,这些参数指定要插入数组的元素,从第一个参数指定的位置开始。例如:
let a = [1,2,3,4,5];
a.splice(2,0,"a","b") // => []; a 现在是 [1,2,"a","b",3,4,5]
a.splice(2,2,[1,2],3) // => ["a","b"]; a 现在是 [1,2,[1,2],3,3,4,5]
请注意,与 concat() 不同,splice() 插入数组本身,而不是这些数组的元素。
fill()
fill() 方法的作用是:将数组元素或数组片段设置为指定值。它对调用它的数组进行修改,并返回修改后的数组:
let a = new Array(5); // 从一个没有元素和长度为5的数组开始
a.fill(0) // => [0,0,0,0,0]; 用零填充数组
a.fill(9, 1) // => [0,9,9,9,9]; 从索引1开始填充9
a.fill(8, 2, -1) // => [0,9,8,8,9]; 在索引2、3处填充8
fill() 的第一个参数是要设置数组元素的值。可选的第二个参数指定起始索引。如果省略,则从索引0开始填充。可选的第三个参数指定结束索引数组元素,填充到(但不包括)此索引。如果省略此参数,则数组将从开始索引填充到结尾。可以通过传递负数来指定相对于数组结尾的索引,就像 slice() 一样。
copyWithin()
copyWithin() 将数组的片段复制到数组中的新位置。它就地修改数组并返回修改后的数组,但不会更改数组的长度。第一个参数指定第一个元素将复制到的目标索引。第二个参数指定要复制的第一个元素的索引。如果省略第二个参数,则使用0。第三个参数指定要复制的元素片段的结尾。如果省略,则使用数组的长度。将复制从起始索引到结束索引(但不包括)的元素。可以通过传递负数来指定相对于数组结尾的索引,就像 slice() 一样:
let a = [1,2,3,4,5];
a.copyWithin(1) // => [1,1,2,3,4]: 将数组元素向上复制一个
a.copyWithin(2, 3, 5) // => [1,1,3,4,4]: 将最后2个元素复制到索引2
a.copyWithin(0, -2) // => [4,4,3,4,4]: 负偏移也起作用
copyWithin() 是一种高性能的方法,对类型化数组特别有用(参见§11.2)。它是根据 C 标准库中的 memmove() 函数建模的。请注意,即使源区域和目标区域之间存在重叠,复制也将正常工作。
7.8.6 数组查找和排序方法
数组实现了 indexOf() 、lastIndexOf() 和 includes() 方法,这些方法与字符串的相同命名方法类似。还有sort() 和 reverse() 方法用于重新排序数组的元素。这些方法将在下面的小节中描述。
indexOf() 和 lastIndexOf()
index() 和 lastIndexOf() 在数组中搜索具有指定值的元素,并返回找到的第一个此类元素的索引,如果找不到,则返回-1。index() 从头到尾搜索数组,lastIndexOf() 从尾到头搜索:
let a = [0,1,2,1,0];
a.indexOf(1) // => 1: a[1] 是 1
a.lastIndexOf(1) // => 3: a[3] 是 1
a.indexOf(3) // => -1: 没有值为3的元素
indexOf() 和 lastIndexOf() 使用 === 运算符将其参数与数组元素进行比较。如果数组包含对象而不是原始值,则这些方法将检查两个引用是否都引用了完全相同的对象。如果要实际查看对象的内容,请尝试将 find() 方法与您自己的自定义谓词函数一起使用。
index() 和 lastIndexOf() 接受可选的第二个参数,该参数指定开始搜索的数组索引。如果省略此参数,则 indexOf() 从开始处开始,lastIndexOf() 从末尾开始。第二个参数允许使用负值,并被视为距数组末尾的偏移量,就像 slice() 方法一样:例如,值–1指定数组的最后一个元素。
下面的函数在数组中搜索指定的值,并返回一个包含所有匹配索引的数组。这演示了如何使用 indexOf() 的第二个参数来查找第一个匹配之外的匹配项。
// 在数组a中查找值x的所有匹配项,并返回一个匹配索引的数组
function findall(a, x) {
let results = [], // 我们将返回的索引数组
len = a.length, // 要搜索的数组的长度
pos = 0; // 要搜索的位置
while (pos < len) { // 还有更多的元素要搜索...
pos = a.indexOf(x, pos); // 搜索
if (pos === -1) break; // 如果什么也没找到,就结束。
results.push(pos); // 否则,将索引存储在数组中
pos = pos + 1; // 从下一个元素开始下一个搜索
}
return results; // 返回索引数组
}
注意,字符串有 indexOf() 和 lastIndexOf() 方法,它们的工作原理与这些数组方法类似,只是第二个负参数被视为零。
includes()
ES2016 includes() 方法接受单个参数,如果数组包含该值,则返回 true,否则返回 false。它不告诉您值的索引,只告诉您它是否存在。includes() 方法实际上是数组的集合成员身份测试。但是请注意,数组并不是集合的有效表示形式,如果使用的元素过多,则应使用 Set 对象(§11.1.1)。
includes() 方法与 index() 方法在一个重要方面略有不同。indexOf() 使用与===运算符相同的算法测试相等性,该算法认为 NaN 与其他所有值(包括它自身)都不同。includes() 使用稍微不同的相等测试版本,该版本将 NaN 视为等于自身。这意味着indexOf() 不会检测数组中的 NaN 值,但 includes() 将会:
let a = [1,true,3,NaN];
a.includes(true) // => true
a.includes(2) // => false
a.includes(NaN) // => true
a.indexOf(NaN) // => -1; indexOf 找不到NaN
sort()
sort() 对数组中的元素进行就地排序,并返回已排序的数组。当 sort() 在没有参数的情况下调用时,它按字母顺序对数组元素进行排序(如果需要,临时将它们转换为字符串以执行比较):
let a = ["banana", "cherry", "apple"];
a.sort(); // a == ["apple", "banana", "cherry"]
如果数组包含 undefined 的元素,则将它们排序到数组的末尾。
若要将数组排序为非字母顺序,必须将比较函数作为参数传递给 sort() 。此函数决定它的两个参数中的哪一个首先出现在排序数组中。如果第一个参数应该出现在第二个参数之前,比较函数应该返回一个小于零的数字。如果排序数组中第一个参数应出现在第二个参数之后,则函数应返回一个大于零的数字。如果两个值相等(即,如果它们的顺序无关),比较函数应该返回 0。因此,例如,要将数组元素按数字而不是字母顺序排序,可以执行以下操作:
let a = [33, 4, 1111, 222];
a.sort(); // a == [1111, 222, 33, 4]; 字母顺序
a.sort(function(a,b) { // 传递比较函数
return a-b; // 根据顺序决定返回 < 0, 0, 或 > 0
}); // a == [4, 33, 222, 1111]; 数字顺序
a.sort((a,b) => b-a); // a == [1111, 222, 33, 4]; 反向数字顺序
作为排序数组项的另一个示例,可以通过传递一个比较函数对字符串数组执行不区分大小写的字母排序,该比较函数在比较字符串数组的参数之前将两个参数都转换为小写(使用 toLowerCase() 方法):
let a = ["ant", "Bug", "cat", "Dog"];
a.sort(); // a == ["Bug","Dog","ant","cat"]; 区分大小写的排序
a.sort(function (s, t) {
let a = s.toLowerCase();
let b = t.toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
}); // a == ["ant","Bug","cat","Dog"]; 不区分大小写排序
reverse()
reverse() 方法的作用是:反转数组元素的顺序并返回反转的数组。它就地执行此操作;换句话说,它不会创建一个重新排列元素的新数组,而是在现有数组中重新排列它们:
let a = [1,2,3];
a.reverse(); // a == [3,2,1]
7.8.7 数组到字符串的转换
数组类定义了三种方法,可以将数组转换为字符串,这通常是创建日志和错误消息时可能会做的事情。(如果要以文本形式保存数组的内容以供以后重用,请使用JSON.stringify() [§6.8]而不是使用此处所述的方法。)
join() 方法的作用是:将数组的所有元素转换为字符串,并将它们连接起来,返回结果字符串。您可以指定一个可选字符串来分隔结果字符串中的元素。如果未指定分隔符字符串,则使用逗号:
let a = [1, 2, 3];
a.join() // => "1,2,3"
a.join(" ") // => "1 2 3"
a.join("") // => "123"
let b = new Array(10); // 长度为10的数组,没有元素
b.join("-") // => "---------": 由9个连字符组成的字符串
join() 方法是 String.split() 方法的相反方法,该方法通过将字符串拆分为多个部分来创建数组。
数组和所有 JavaScript 对象一样,都有一个 toString() 方法。对于数组,这个方法的工作方式与 join() 方法一样,没有参数:
[1,2,3].toString() // => "1,2,3"
["a", "b", "c"].toString() // => "a,b,c"
[1, [2,"c"]].toString() // => "1,2,c"
请注意,输出不包括方括号或任何其他类型的分隔符。
toLocaleString() 是 toString() 的本地化版本。它通过调用元素的 toLocaleString() 方法将每个数组元素转换为字符串,然后使用特定于语言环境(和实现定义)的分隔符字符串连接结果字符串。
7.8.8 静态数组函数
除了我们已经记录的数组方法之外,Array 类还定义了三个静态函数,可以通过数组构造函数而不是数组调用它们。Array.of() 和 Array.from() 是用于创建新数组的工厂方法。它们记录在§7.1.4和§7.1.5中。
另一个静态数组函数是 Array.isArray(),可用于确定未知值是否为数组:
Array.isArray([]) // => true
Array.isArray({}) // => false
7.9 类数组对象
如前所述,JavaScript 数组具有其他对象所不具备的一些特性:
- 当向列表中添加新元素时,“length”属性将自动更新。
- 将 length 设置为较小的值将截断数组。
- 从 Array.prototype 中继承一些有用的方法。
- Array.isArray() 对于数组返回 true。
这些特性使 JavaScript 数组不同于常规对象。但它们并不是定义数组的基本特性。将具有数值 length 属性和相应的非负整数属性的任何对象视为一种数组通常是完全合理的。
实际上,这些“类似数组”的对象在实践中偶尔会出现,尽管您不能直接对它们调用数组方法,也不能期望从length属性中获得特殊行为,但是您仍然可以使用与真正数组相同的代码来遍历它们。事实证明,许多数组算法在处理类似数组的对象时和在实际数组中一样有效。如果算法将数组视为只读,或者至少保持数组长度不变,则这一点尤为正确。
以下代码获取一个常规对象,添加属性使其成为类似数组的对象,然后迭代生成的伪数组的“元素”:
let a = {}; // 从一个常规的空对象开始
// 添加属性使其成为“类数组”
let i = 0;
while (i < 10) {
a[i] = i * i;
i++;
}
a.length = i;
// 现在迭代它,好像它是一个真正的数组
let total = 0;
for (let j = 0; j < a.length; j++) {
total += a[j];
}
在客户端 JavaScript 中,处理 HTML 文档的许多方法(如 document.querySelectorAll(),例如)返回类似数组的对象。下面是一个函数,您可以使用它来测试类似数组的对象:
// 确定o是否是一个类似数组的对象。
// 字符串和函数具有数字长度属性,但是
// 排除在测试类型之外。在客户端JavaScript中,DOM文本
// 节点具有数值长度属性,可能需要排除
// 用一个额外的o.nodeType!==3测试。
function isArrayLike(o) {
if (o && // o不为空、未定义等。
typeof o === "object" && // o是一个对象
Number.isFinite(o.length) && // o.length 是一个有限的数
o.length >= 0 && // o.length 非负
Number.isInteger(o.length) && // o.length 是整数
o.length < 4294967295) { // o.length < 2^32 - 1
return true; // 那么o是类数组
} else {
return false; // 否则不是
}
}
我们将在后面的小节中看到字符串的行为类似于数组。尽管如此,对于类似数组的对象,像这样的测试对于字符串通常返回 false,它们通常最好作为字符串处理,而不是作为数组处理。
大多数 JavaScript 数组方法都是有目的地定义为泛型的,以便除了真正的数组外,它们还可以在应用于类似数组的对象时正常工作。因为类数组的对象不继承自 Array.prototype,则不能直接对其调用数组方法。但是您可以使用 Function.call() 方法(详见§8.7.4):
let a = {"0": "a", "1": "b", "2": "c", length: 3}; // 一个类数组对象
Array.prototype.join.call(a, "+") // => "a+b+c"
Array.prototype.map.call(a, x => x.toUpperCase()) // => ["A","B","C"]
Array.prototype.slice.call(a, 0) // => ["a","b","c"]: 真正的数组拷贝
Array.from(a) // => ["a","b","c"]: 更简单的数组拷贝
这段代码倒数第二行在类数组对象上调用 Array.slice() 方法,以便将该对象的元素复制到一个真正的数组对象中。这是一个惯用的技巧,存在于许多遗留代码中,但现在使用 Array.from() 更容易。
7.10 字符串作为数组
JavaScript 字符串的行为类似于 UTF-16 unicode字符的只读数组。您可以使用方括号,而不是使用 charAt() 方法访问单个字符:
let s = "test";
s.charAt(0) // => "t"
s[1] // => "e"
当然,typeof 操作符对字符串操作仍然返回“string”,并且给 Array.isArray() 方法传递字符串,则返回 false。
可索引字符串的主要好处是,我们可以用方括号替换对 charAt() 的调用,方括号更简洁、可读,而且可能更高效。然而,字符串的行为类似数组的事实也意味着我们可以将通用数组方法应用到它们上。例如:
Array.prototype.join.call("JavaScript", " ") // => "J a v a S c r i p t"
请记住,字符串是不可变的值,因此当它们被视为数组时,它们是只读数组。push()、sort()、reverse() 和 splice()等数组方法就地修改数组,而不处理字符串。但是,尝试使用数组方法修改字符串不会导致错误:它只是默默地失败。
7.11 总结
本章深入讨论了 JavaScript 数组,包括关于稀疏数组和类数组对象的深奥细节。本章的要点是:
- 数组字面量是以逗号分隔的值列表的形式写入方括号中。
- 通过在方括号内指定所需的数组索引来访问各个数组元素。
- for/of 循环和ES6中引入的 … 扩展运算符是迭代数组的特别有用的方法。
- Array 类为操作数组定义了一组丰富的方法,您应该熟悉 Array API。