索引集合
本章介绍按索引值排序的数据集合。这包括数组和类似数组的结构,如 Array
对象和 TypedArray
对象。
数组 是一个按顺序排列的值列表,你可以用名称和索引来引用它们。
例如,考虑一个名为 emp
的数组,它包含按员工编号索引的员工姓名。所以 emp[0]
将是员工编号为零的员工,emp[1]
将是员工编号为一的员工,依此类推。
JavaScript 没有明确的数组数据类型。但是,你可以使用预定义的 Array
对象及其方法在应用程序中使用数组。Array
对象具有用于以各种方式操作数组的方法,例如连接、反转和排序。它有一个用于确定数组长度的属性,以及其他用于正则表达式的属性。
本文将重点关注数组,但许多相同的概念也适用于类型化数组,因为数组和类型化数组共享许多类似的方法。有关类型化数组的更多信息,请参阅 类型化数组指南。
创建数组
以下语句创建了等效的数组
const arr1 = new Array(element0, element1, /* …, */ elementN);
const arr2 = Array(element0, element1, /* …, */ elementN);
const arr3 = [element0, element1, /* …, */ elementN];
element0, element1, …, elementN
是数组元素值的列表。当指定这些值时,数组将使用它们作为数组元素进行初始化。数组的 length
属性设置为参数的数量。
方括号语法称为“数组字面量”或“数组初始化器”。它比其他形式的数组创建更短,因此通常是首选。有关详细信息,请参阅 数组字面量。
要创建一个非零长度的数组,但不包含任何项目,可以使用以下任一方法
// This...
const arr1 = new Array(arrayLength);
// ...results in the same array as this
const arr2 = Array(arrayLength);
// This has exactly the same effect
const arr3 = [];
arr3.length = arrayLength;
注意:在上面的代码中,arrayLength
必须是 Number
。否则,将创建一个只有一个元素(提供的值)的数组。调用 arr.length
将返回 arrayLength
,但数组不包含任何元素。for...in
循环不会在数组上找到任何属性。
除了上面显示的新定义的变量之外,数组还可以作为新对象或现有对象的属性被分配
const obj = {};
// …
obj.prop = [element0, element1, /* …, */ elementN];
// OR
const obj = { prop: [element0, element1, /* …, */ elementN] };
如果你希望使用单个元素初始化一个数组,并且该元素恰好是 Number
,则必须使用方括号语法。当单个 Number
值传递给 Array()
构造函数或函数时,它被解释为 arrayLength
,而不是单个元素。
这将创建一个只有一个元素的数组:数字 42。
const arr = [42];
这将创建一个没有元素的数组,并将 arr.length
设置为 42。
const arr = Array(42);
这等效于
const arr = [];
arr.length = 42;
如果 N
是一个非整数,其小数部分不为零,则调用 Array(N)
会导致 RangeError
。以下示例说明了这种行为。
const arr = Array(9.3); // RangeError: Invalid array length
如果你的代码需要创建具有单个任意数据类型元素的数组,那么使用数组字面量更安全。或者,先创建一个空数组,然后再向其中添加单个元素。
你也可以使用 Array.of
静态方法来创建具有单个元素的数组。
const wisenArray = Array.of(9.3); // wisenArray contains only one element 9.3
引用数组元素
填充数组
你可以通过将值分配给数组的元素来填充数组。例如
const emp = [];
emp[0] = "Casey Jones";
emp[1] = "Phil Lesh";
emp[2] = "August West";
注意:如果你在上面的代码中向数组操作符提供一个非整数的值,那么将创建一个表示数组的对象中的属性,而不是一个数组元素。
const arr = [];
arr[3.4] = "Oranges";
console.log(arr.length); // 0
console.log(Object.hasOwn(arr, 3.4)); // true
你也可以在创建数组时填充数组
const myArray = new Array("Hello", myVar, 3.14159);
// OR
const myArray = ["Mango", "Apple", "Orange"];
了解长度
在实现级别,JavaScript 数组实际上将它们的元素存储为标准对象属性,使用数组索引作为属性名称。
length
属性是特殊的。它的值始终是一个正整数,大于最后一个元素的索引(如果存在)。(在下面的示例中,'Dusty'
在 30
处被索引,所以 cats.length
返回 30 + 1
)。
请记住,JavaScript 数组索引是基于 0 的:它们从 0
开始,而不是 1
。这意味着 length
属性将比存储在数组中的最高索引多一
const cats = [];
cats[30] = ["Dusty"];
console.log(cats.length); // 31
你也可以将值分配给 length
属性。
写入的值短于存储项目的数量会截断数组。写入 0
会完全清空它
const cats = ["Dusty", "Misty", "Twiggy"];
console.log(cats.length); // 3
cats.length = 2;
console.log(cats); // [ 'Dusty', 'Misty' ] - Twiggy has been removed
cats.length = 0;
console.log(cats); // []; the cats array is empty
cats.length = 3;
console.log(cats); // [ <3 empty items> ]
遍历数组
一个常见的操作是遍历数组的值,以某种方式处理每个值。最简单的方法如下
const colors = ["red", "green", "blue"];
for (let i = 0; i < colors.length; i++) {
console.log(colors[i]);
}
如果你知道你的数组中的元素在布尔上下文中都不会评估为 false
——例如,如果你的数组仅包含 DOM 节点——则可以使用更高效的习惯用法
const divs = document.getElementsByTagName("div");
for (let i = 0, div; (div = divs[i]); i++) {
/* Process div in some way */
}
这避免了检查数组长度的开销,并确保 div
变量在每次循环时都被重新分配给当前项目,以方便使用。
forEach()
方法提供了另一种遍历数组的方法
const colors = ["red", "green", "blue"];
colors.forEach((color) => console.log(color));
// red
// green
// blue
传递给 forEach
的函数对数组中的每个项目执行一次,将数组项目作为函数的参数传递。未赋值的值不会在 forEach
循环中进行迭代。
请注意,在定义数组时省略的数组元素在使用 forEach
迭代时不会被列出,但在将 undefined
手动分配给元素时会被列出
const sparseArray = ["first", "second", , "fourth"];
sparseArray.forEach((element) => {
console.log(element);
});
// Logs:
// first
// second
// fourth
if (sparseArray[2] === undefined) {
console.log("sparseArray[2] is undefined"); // true
}
const nonsparseArray = ["first", "second", undefined, "fourth"];
nonsparseArray.forEach((element) => {
console.log(element);
});
// Logs:
// first
// second
// undefined
// fourth
由于 JavaScript 数组元素被保存为标准对象属性,因此不建议使用 for...in
循环遍历 JavaScript 数组,因为将列出普通元素和所有可枚举属性。
数组方法
Array
对象具有以下方法
concat()
方法连接两个或多个数组,并返回一个新数组。
let myArray = ["1", "2", "3"];
myArray = myArray.concat("a", "b", "c");
// myArray is now ["1", "2", "3", "a", "b", "c"]
join()
方法 将数组的所有元素连接成一个字符串。
const myArray = ["Wind", "Rain", "Fire"];
const list = myArray.join(" - "); // list is "Wind - Rain - Fire"
push()
方法 向数组末尾添加一个或多个元素,并返回数组的最终 length
。
const myArray = ["1", "2"];
myArray.push("3"); // myArray is now ["1", "2", "3"]
pop()
方法 从数组中删除最后一个元素,并返回该元素。
const myArray = ["1", "2", "3"];
const last = myArray.pop();
// myArray is now ["1", "2"], last = "3"
shift()
方法 从数组中删除第一个元素,并返回该元素。
const myArray = ["1", "2", "3"];
const first = myArray.shift();
// myArray is now ["2", "3"], first is "1"
unshift()
方法 向数组开头添加一个或多个元素,并返回数组的新的长度。
const myArray = ["1", "2", "3"];
myArray.unshift("4", "5");
// myArray becomes ["4", "5", "1", "2", "3"]
slice()
方法 从数组中提取一部分,并返回一个新的数组。
let myArray = ["a", "b", "c", "d", "e"];
myArray = myArray.slice(1, 4); // [ "b", "c", "d"]
// starts at index 1 and extracts all elements
// until index 3
at()
方法 返回数组中指定索引处的元素,如果索引超出范围,则返回 undefined
。它通常用于负索引,从数组末尾访问元素。
const myArray = ["a", "b", "c", "d", "e"];
myArray.at(-2); // "d", the second-last element of myArray
splice()
方法 从数组中删除元素,并(可选)替换它们。它返回从数组中删除的项目。
const myArray = ["1", "2", "3", "4", "5"];
myArray.splice(1, 3, "a", "b", "c", "d");
// myArray is now ["1", "a", "b", "c", "d", "5"]
// This code started at index one (or where the "2" was),
// removed 3 elements there, and then inserted all consecutive
// elements in its place.
reverse()
方法 原地颠倒数组元素:第一个数组元素变为最后一个,最后一个变为第一个。它返回对数组的引用。
const myArray = ["1", "2", "3"];
myArray.reverse();
// transposes the array so that myArray = ["3", "2", "1"]
flat()
方法 返回一个新的数组,其中包含所有子数组元素,这些元素递归地连接到指定的深度。
let myArray = [1, 2, [3, 4]];
myArray = myArray.flat();
// myArray is now [1, 2, 3, 4], since the [3, 4] subarray is flattened
sort()
方法 原地对数组元素进行排序,并返回对数组的引用。
const myArray = ["Wind", "Rain", "Fire"];
myArray.sort();
// sorts the array so that myArray = ["Fire", "Rain", "Wind"]
sort()
也可以接受一个回调函数来确定如何比较数组元素。回调函数用两个参数调用,这两个参数是数组中的两个值。该函数比较这两个值,并返回一个正数、负数或零,表示这两个值的顺序。例如,以下将按字符串的最后一个字母对数组进行排序
const sortFn = (a, b) => {
if (a[a.length - 1] < b[b.length - 1]) {
return -1; // Negative number => a < b, a comes before b
} else if (a[a.length - 1] > b[b.length - 1]) {
return 1; // Positive number => a > b, a comes after b
}
return 0; // Zero => a = b, a and b keep their original order
};
myArray.sort(sortFn);
// sorts the array so that myArray = ["Wind","Fire","Rain"]
- 如果
a
在排序系统中小于b
,则返回-1
(或任何负数) - 如果
a
在排序系统中大于b
,则返回1
(或任何正数) - 如果
a
和b
被认为等价,则返回0
。
indexOf()
方法 在数组中搜索 searchElement
,并返回第一个匹配项的索引。
const a = ["a", "b", "a", "b", "a"];
console.log(a.indexOf("b")); // 1
// Now try again, starting from after the last match
console.log(a.indexOf("b", 2)); // 3
console.log(a.indexOf("z")); // -1, because 'z' was not found
lastIndexOf()
方法 的工作方式类似于 indexOf
,但从末尾开始并向后搜索。
const a = ["a", "b", "c", "d", "a", "b"];
console.log(a.lastIndexOf("b")); // 5
// Now try again, starting from before the last match
console.log(a.lastIndexOf("b", 4)); // 1
console.log(a.lastIndexOf("z")); // -1
forEach()
方法 对每个数组项执行 callback
,并返回 undefined
。
const a = ["a", "b", "c"];
a.forEach((element) => {
console.log(element);
});
// Logs:
// a
// b
// c
forEach
方法(以及下面的其他方法)接受回调函数,被称为迭代方法,因为它们以某种方式迭代整个数组。每个方法都接受一个可选的第二个参数,称为 thisArg
。如果提供,thisArg
将成为回调函数主体内部 this
关键字的值。如果没有提供,与其他情况下在显式对象上下文中之外调用函数一样,this
将在函数为非严格时引用全局对象(window
、globalThis
等),或在函数为严格时引用 undefined
。
注意:上面介绍的 sort()
方法不是迭代方法,因为它的回调函数仅用于比较,并且可能不会根据元素顺序按任何特定顺序调用。sort()
也不接受 thisArg
参数。
map()
方法 返回一个新的数组,该数组包含对每个数组项执行 callback
返回的值。
const a1 = ["a", "b", "c"];
const a2 = a1.map((item) => item.toUpperCase());
console.log(a2); // ['A', 'B', 'C']
flatMap()
方法 在 map()
之后运行 flat()
,深度为 1。
const a1 = ["a", "b", "c"];
const a2 = a1.flatMap((item) => [item.toUpperCase(), item.toLowerCase()]);
console.log(a2); // ['A', 'a', 'B', 'b', 'C', 'c']
filter()
方法 返回一个新的数组,其中包含 callback
返回 true
的项。
const a1 = ["a", 10, "b", 20, "c", 30];
const a2 = a1.filter((item) => typeof item === "number");
console.log(a2); // [10, 20, 30]
find()
方法 返回 callback
返回 true
的第一个项。
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.find((item) => typeof item === "number");
console.log(i); // 10
findLast()
方法 返回 callback
返回 true
的最后一个项。
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLast((item) => typeof item === "number");
console.log(i); // 30
findIndex()
方法 返回 callback
返回 true
的第一个项的索引。
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findIndex((item) => typeof item === "number");
console.log(i); // 1
findLastIndex()
方法 返回 callback
返回 true
的最后一个项的索引。
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLastIndex((item) => typeof item === "number");
console.log(i); // 5
every()
方法 如果 callback
对数组中的每个项都返回 true
,则返回 true
。
function isNumber(value) {
return typeof value === "number";
}
const a1 = [1, 2, 3];
console.log(a1.every(isNumber)); // true
const a2 = [1, "2", 3];
console.log(a2.every(isNumber)); // false
some()
方法 如果 callback
对数组中的至少一个项返回 true
,则返回 true
。
function isNumber(value) {
return typeof value === "number";
}
const a1 = [1, 2, 3];
console.log(a1.some(isNumber)); // true
const a2 = [1, "2", 3];
console.log(a2.some(isNumber)); // true
const a3 = ["1", "2", "3"];
console.log(a3.some(isNumber)); // false
reduce()
方法 对数组中的每个值应用 callback(accumulator, currentValue, currentIndex, array)
,目的是将项目列表减少到单个值。reduce
函数返回 callback
函数返回的最终值。
如果指定了 initialValue
,则 callback
使用 initialValue
作为第一个参数值,并使用数组中第一个项的值作为第二个参数值进行调用。
如果没有指定 initialValue
,则 callback
的前两个参数值将是数组的第一个和第二个元素。在每次后续调用中,第一个参数的值将是 callback
在前一次调用中返回的值,第二个参数的值将是数组中的下一个值。
如果 callback
需要访问正在处理的项目的索引,或访问整个数组,则它们可以作为可选参数使用。
const a = [10, 20, 30];
const total = a.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0,
);
console.log(total); // 60
reduceRight()
方法 的工作方式类似于 reduce()
,但从最后一个元素开始。
reduce
和 reduceRight
是迭代数组方法中最不直观的。它们应该用于递归地组合两个值的算法,以便将序列减少为单个值。
数组转换
您可以将数组与其他数据结构相互转换。
对数组元素进行分组
Object.groupBy()
方法 可以用来对数组元素进行分组,使用测试函数返回一个字符串,表示当前元素所属的组。
这里我们有一个简单的库存数组,包含具有 name
和 type
的 "food" 对象。
const inventory = [
{ name: "asparagus", type: "vegetables" },
{ name: "bananas", type: "fruit" },
{ name: "goat", type: "meat" },
{ name: "cherries", type: "fruit" },
{ name: "fish", type: "meat" },
];
要使用 Object.groupBy()
,您需要提供一个回调函数,该函数使用当前元素(以及可选的当前索引和数组)作为参数,并返回一个字符串,表示元素所属的组。
下面的代码使用箭头函数来返回每个数组元素的 type
(这使用 函数参数的对象解构语法 从传递的对象中解构 type
元素)。结果是一个对象,其属性名称来自回调函数返回的唯一字符串。每个属性都被分配一个数组,该数组包含该组中的元素。
const result = Object.groupBy(inventory, ({ type }) => type);
console.log(result);
// Logs
// {
// vegetables: [{ name: 'asparagus', type: 'vegetables' }],
// fruit: [
// { name: 'bananas', type: 'fruit' },
// { name: 'cherries', type: 'fruit' }
// ],
// meat: [
// { name: 'goat', type: 'meat' },
// { name: 'fish', type: 'meat' }
// ]
// }
请注意,返回的对象引用的是与原始数组相同的元素(不是深拷贝)。更改这些元素的内部结构将反映在原始数组和返回的对象中。
如果您不能使用字符串作为键,例如,如果与组关联的信息与可能发生更改的对象相关联,那么您可以改用 Map.groupBy()
。这与 Object.groupBy()
非常相似,只是它将数组元素分组到一个 Map
中,该 Map
可以使用任意值(对象 或 基本类型)作为键。
稀疏数组
数组可以包含 "空插槽",这与用 undefined
值填充的插槽不同。空插槽可以通过以下方式创建
// Array constructor:
const a = Array(5); // [ <5 empty items> ]
// Consecutive commas in array literal:
const b = [1, 2, , , 5]; // [ 1, 2, <2 empty items>, 5 ]
// Directly setting a slot with index greater than array.length:
const c = [1, 2];
c[4] = 5; // [ 1, 2, <2 empty items>, 5 ]
// Elongating an array by directly setting .length:
const d = [1, 2];
d.length = 5; // [ 1, 2, <3 empty items> ]
// Deleting an element:
const e = [1, 2, 3, 4, 5];
delete e[2]; // [ 1, 2, <1 empty item>, 4, 5 ]
在某些操作中,空插槽的行为就像它们用 undefined
填充一样。
const arr = [1, 2, , , 5]; // Create a sparse array
// Indexed access
console.log(arr[2]); // undefined
// For...of
for (const i of arr) {
console.log(i);
}
// Logs: 1 2 undefined undefined 5
// Spreading
const another = [...arr]; // "another" is [ 1, 2, undefined, undefined, 5 ]
但在其他操作中(最重要的是数组迭代方法),空插槽会被跳过。
const mapped = arr.map((i) => i + 1); // [ 2, 3, <2 empty items>, 6 ]
arr.forEach((i) => console.log(i)); // 1 2 5
const filtered = arr.filter(() => true); // [ 1, 2, 5 ]
const hasFalsy = arr.some((k) => !k); // false
// Property enumeration
const keys = Object.keys(arr); // [ '0', '1', '4' ]
for (const key in arr) {
console.log(key);
}
// Logs: '0' '1' '4'
// Spreading into an object uses property enumeration, not the array's iterator
const objectSpread = { ...arr }; // { '0': 1, '1': 2, '4': 5 }
有关数组方法如何处理稀疏数组的完整列表,请参阅 Array
参考页面。
多维数组
数组可以嵌套,这意味着一个数组可以包含另一个数组作为元素。利用 JavaScript 数组的这一特性,可以创建多维数组。
以下代码创建了一个二维数组。
const a = new Array(4);
for (let i = 0; i < 4; i++) {
a[i] = new Array(4);
for (let j = 0; j < 4; j++) {
a[i][j] = `[${i}, ${j}]`;
}
}
此示例创建一个具有以下行的数组
Row 0: [0, 0] [0, 1] [0, 2] [0, 3] Row 1: [1, 0] [1, 1] [1, 2] [1, 3] Row 2: [2, 0] [2, 1] [2, 2] [2, 3] Row 3: [3, 0] [3, 1] [3, 2] [3, 3]
使用数组存储其他属性
数组也可以像对象一样使用,用来存储相关信息。
const arr = [1, 2, 3];
arr.property = "value";
console.log(arr.property); // "value"
例如,当一个数组是正则表达式与字符串匹配的结果时,该数组返回属性和元素,提供有关匹配的信息。数组是 RegExp.prototype.exec()
、String.prototype.match()
和 String.prototype.split()
的返回值。有关将数组与正则表达式一起使用的信息,请参阅 正则表达式。
使用类数组对象
某些 JavaScript 对象,例如 NodeList
(由 document.getElementsByTagName()
返回)或在函数主体内部可用的 arguments
对象,在表面上看起来和行为都像数组,但没有共享所有方法。arguments
对象提供了一个 length
属性,但没有实现像 forEach()
这样的数组方法。
数组方法不能直接在类似数组的对象上调用。
function printArguments() {
arguments.forEach((item) => {
console.log(item);
}); // TypeError: arguments.forEach is not a function
}
但是,您可以使用 Function.prototype.call()
间接调用它们。
function printArguments() {
Array.prototype.forEach.call(arguments, (item) => {
console.log(item);
});
}
数组原型方法也可以用在字符串上,因为它们以类似于数组的方式提供对字符的顺序访问。
Array.prototype.forEach.call("a string", (chr) => {
console.log(chr);
});