索引集合

本章介绍按索引值排序的数据集合。这包括数组和类数组结构,例如 Array 对象和 TypedArray 对象。

数组是值的有序列表,你可以通过名称和索引来引用它们。

例如,考虑一个名为 emp 的数组,它包含按数字员工编号索引的员工姓名。因此,emp[0] 将是零号员工,emp[1] 是一号员工,依此类推。

JavaScript 没有显式的数组数据类型。但是,你可以使用预定义的 Array 对象及其方法在你的应用程序中处理数组。Array 对象具有多种操作数组的方法,例如连接、反转和排序。它有一个用于确定数组长度的属性以及其他用于正则表达式的属性。

本文将重点介绍数组,但许多相同的概念也适用于类型化数组,因为数组和类型化数组共享许多相似的方法。有关类型化数组的更多信息,请参阅类型化数组指南

创建数组

以下语句创建等效数组

js
const arr1 = new Array(element0, element1, /* …, */ elementN);
const arr2 = Array(element0, element1, /* …, */ elementN);
const arr3 = [element0, element1, /* …, */ elementN];

element0, element1, ..., elementN 是数组元素的列表。指定这些值后,数组将使用它们作为数组的元素进行初始化。数组的 length 属性设置为参数的数量。

方括号语法被称为“数组字面量”或“数组初始化器”。它比其他形式的数组创建更短,因此通常更受推荐。有关详细信息,请参阅数组字面量

要创建非零长度但没有任何项的数组,可以使用以下任一方法:

js
// 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 循环将不会在数组上找到任何属性。

除了如上所示的新定义变量外,数组还可以作为新对象或现有对象的属性进行赋值

js
const obj = {};
// …
obj.prop = [element0, element1, /* …, */ elementN];

// OR
const obj = { prop: [element0, element1, /* …, */ elementN] };

如果你想用单个元素初始化一个数组,并且该元素恰好是一个 Number,你必须使用方括号语法。当一个单个 Number 值传递给 Array() 构造函数或函数时,它被解释为 arrayLength,而不是单个元素。

这将创建一个只包含一个元素(数字 42)的数组。

js
const arr = [42];

这将创建一个没有元素且 arr.length 设置为 42 的数组。

js
const arr = Array(42);

这等价于

js
const arr = [];
arr.length = 42;

如果 N 是一个非整数且其小数部分不为零的数字,调用 Array(N) 会导致 RangeError。以下示例说明了此行为。

js
const arr = Array(9.3); // RangeError: Invalid array length

如果你的代码需要创建包含任意数据类型单个元素的数组,使用数组字面量会更安全。或者,先创建一个空数组,然后再添加单个元素。

你也可以使用 Array.of 静态方法创建包含单个元素的数组。

js
const arr = Array.of(9.3); // arr contains only one element 9.3

引用数组元素

由于元素也是属性,你可以使用属性访问器访问它们。假设你定义了以下数组:

js
const myArray = ["Wind", "Rain", "Fire"];

你可以将数组的第一个元素称为 myArray[0],第二个元素称为 myArray[1],依此类推……元素的索引从零开始。

注意:你也可以使用属性访问器访问数组的其他属性,就像使用对象一样。

js
const arr = ["one", "two", "three"];
arr[2]; // three
arr["length"]; // 3

填充数组

你可以通过给数组元素赋值来填充数组。例如:

js
const emp = [];
emp[0] = "Casey Jones";
emp[1] = "Phil Lesh";
emp[2] = "August West";

注意:如果你在上述代码中向数组操作符提供一个非整数值,则会在表示数组的对象中创建一个属性,而不是数组元素。

js
const arr = [];
arr[3.4] = "Oranges";
console.log(arr.length); // 0
console.log(Object.hasOwn(arr, 3.4)); // true

你也可以在创建数组时填充它

js
const myArray = new Array("Hello", myVar, 3.14159);
// OR
const myArray = ["Mango", "Apple", "Orange"];

理解长度

在实现层面,JavaScript 的数组实际上将其元素作为标准对象属性存储,并使用数组索引作为属性名。

length 属性是特殊的。如果存在最后一个元素,其值始终是一个大于该元素索引的正整数。(在下面的示例中,'Dusty' 的索引是 30,因此 cats.length 返回 30 + 1)。

请记住,JavaScript 数组索引是 0-based:它们从 0 开始,而不是 1。这意味着 length 属性将比数组中存储的最高索引大一

js
const cats = [];
cats[30] = ["Dusty"];
console.log(cats.length); // 31

你也可以给 length 属性赋值。

写入一个比存储项数量短的值会截断数组。写入 0 会完全清空数组

js
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> ]

迭代数组

一种常见的操作是遍历数组的值,并以某种方式处理每个值,如下所示:

js
const colors = ["red", "green", "blue"];
for (let i = 0; i < colors.length; i++) {
  console.log(colors[i]);
}

如果你知道数组中的任何元素在布尔上下文中都不会评估为 false(例如,如果你的数组只包含 DOM 节点),你可以使用更高效的惯用语:

js
const divs = document.getElementsByTagName("div");
for (let i = 0, div; (div = divs[i]); i++) {
  /* Process div in some way */
}

这避免了检查数组长度的开销,并确保 div 变量在每次循环时都被重新分配给当前项,从而增加了便利性。

forEach() 方法提供了另一种遍历数组的方式:

js
const colors = ["red", "green", "blue"];
colors.forEach((color) => console.log(color));
// red
// green
// blue

传递给 forEach 的函数会为数组中的每个项执行一次,并将数组项作为参数传递给该函数。未赋值的值不会在 forEach 循环中迭代。

请注意,当数组定义时省略的数组元素在通过 forEach 迭代时不会被列出,但如果手动将 undefined 赋给该元素,则被列出。

js
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() 方法连接两个或多个数组并返回一个新数组。

js
let myArray = ["1", "2", "3"];
myArray = myArray.concat("a", "b", "c");
// myArray is now ["1", "2", "3", "a", "b", "c"]

join() 方法将数组的所有元素连接成一个字符串。

js
const myArray = ["Wind", "Rain", "Fire"];
const list = myArray.join(" - "); // list is "Wind - Rain - Fire"

push() 方法向数组末尾添加一个或多个元素,并返回数组的最终 length

js
const myArray = ["1", "2"];
myArray.push("3"); // myArray is now ["1", "2", "3"]

pop() 方法从数组中移除最后一个元素并返回该元素。

js
const myArray = ["1", "2", "3"];
const last = myArray.pop();
// myArray is now ["1", "2"], last = "3"

shift() 方法从数组中移除第一个元素并返回该元素。

js
const myArray = ["1", "2", "3"];
const first = myArray.shift();
// myArray is now ["2", "3"], first is "1"

unshift() 方法向数组前端添加一个或多个元素,并返回数组的新长度。

js
const myArray = ["1", "2", "3"];
myArray.unshift("4", "5");
// myArray becomes ["4", "5", "1", "2", "3"]

slice() 方法提取数组的一部分并返回一个新数组。

js
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。它主要用于访问数组末尾元素的负索引。

js
const myArray = ["a", "b", "c", "d", "e"];
myArray.at(-2); // "d", the second-last element of myArray

splice() 方法从数组中移除元素并(可选地)替换它们。它返回从数组中移除的项。

js
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() 方法将数组元素就地转置:第一个数组元素变为最后一个,最后一个变为第一个。它返回对数组的引用。

js
const myArray = ["1", "2", "3"];
myArray.reverse();
// transposes the array so that myArray = ["3", "2", "1"]

flat() 方法返回一个新数组,其中所有子数组元素都递归地连接到其中,直到指定深度。

js
let myArray = [1, 2, [3, 4]];
myArray = myArray.flat();
// myArray is now [1, 2, 3, 4], since the [3, 4] subarray is flattened

sort() 方法将数组元素就地排序,并返回对数组的引用。

js
const myArray = ["Wind", "Rain", "Fire"];
myArray.sort();
// sorts the array so that myArray = ["Fire", "Rain", "Wind"]

sort() 还可以接受一个回调函数来决定如何比较数组元素。回调函数以两个参数调用,这两个参数是数组中的两个值。该函数比较这两个值并返回一个正数、负数或零,指示这两个值的顺序。例如,以下代码将按字符串的最后一个字母对数组进行排序:

js
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(或任何正数)
  • 如果 ab 被认为是等效的,则返回 0

indexOf() 方法在数组中搜索 searchElement 并返回第一个匹配项的索引。

js
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,但从末尾开始向后搜索。

js
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

js
const a = ["a", "b", "c"];
a.forEach((element) => {
  console.log(element);
});
// Logs:
// a
// b
// c

接受回调的 forEach 方法(以及下面的其他方法)被称为迭代方法,因为它们以某种方式遍历整个数组。每个方法都接受一个可选的第二个参数,称为 thisArg。如果提供了 thisArg,则 thisArg 成为回调函数体内部 this 关键字的值。如果未提供,与其他在显式对象上下文之外调用函数的情况一样,当函数不是严格模式时,this 将引用全局对象(windowglobalThis 等),当函数是严格模式时,this 将是 undefined

注意:上面介绍的 sort() 方法不是迭代方法,因为它的回调函数仅用于比较,并且可能不会根据元素顺序以特定顺序调用。sort() 也不接受 thisArg 参数。

map() 方法返回一个新数组,其中包含在每个数组项上执行 callback 的返回值。

js
const a1 = ["a", "b", "c"];
const a2 = a1.map((item) => item.toUpperCase());
console.log(a2); // ['A', 'B', 'C']

flatMap() 方法执行 map(),然后执行深度为 1 的 flat()

js
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 的项。

js
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 的第一个项。

js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.find((item) => typeof item === "number");
console.log(i); // 10

findLast() 方法返回 callback 返回 true 的最后一个项。

js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLast((item) => typeof item === "number");
console.log(i); // 30

findIndex() 方法返回 callback 返回 true 的第一个项的索引。

js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findIndex((item) => typeof item === "number");
console.log(i); // 1

findLastIndex() 方法返回 callback 返回 true 的最后一个项的索引。

js
const a1 = ["a", 10, "b", 20, "c", 30];
const i = a1.findLastIndex((item) => typeof item === "number");
console.log(i); // 5

every() 方法在数组中每个项的 callback 都返回 true 时返回 true

js
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

js
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 需要访问正在处理的项的索引,或访问整个数组,它们都可以作为可选参数提供。

js
const a = [10, 20, 30];
const total = a.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  0,
);
console.log(total); // 60

reduceRight() 方法的工作方式与 reduce() 类似,但从最后一个元素开始。

reducereduceRight 是迭代数组方法中最不明显的。它们应该用于递归地组合两个值以将序列缩减为单个值的算法。

数组转换

你可以在数组和其他数据结构之间来回转换。

对数组元素进行分组

可以使用 Object.groupBy() 方法对数组的元素进行分组,使用一个测试函数,该函数返回一个字符串,指示当前元素的组。

这里我们有一个包含具有 nametype 的“食物”对象的库存数组。

js
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 元素)。结果是一个对象,其属性以回调函数返回的唯一字符串命名。每个属性都被分配一个包含该组中元素的数组。

js
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 值的槽不同。空槽可以通过以下方式之一创建:

js
// 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 填充一样。

js
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 ]

但在其他操作中(最显著的是数组迭代方法),空槽会被跳过。

js
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 数组的这一特性,可以创建多维数组。

以下代码创建一个二维数组。

js
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]

使用数组存储其他属性

数组也可以像对象一样使用,用于存储相关信息。

js
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() 这样的数组方法。

数组方法不能直接在类数组对象上调用。

js
function printArguments() {
  arguments.forEach((item) => {
    console.log(item);
  }); // TypeError: arguments.forEach is not a function
}

但你可以使用 Function.prototype.call() 间接调用它们。

js
function printArguments() {
  Array.prototype.forEach.call(arguments, (item) => {
    console.log(item);
  });
}

数组原型方法也可以用于字符串,因为它们以类似于数组的方式提供对其字符的顺序访问

js
Array.prototype.forEach.call("a string", (chr) => {
  console.log(chr);
});