在 JavaScript 中,数组 (Arrays) 是一种特殊类型的对象,用于存储有序的数据集合。它们是日常编程中最常用和最强大的数据结构之一,无论是存储用户列表、记录数据流、处理表单输入,还是构建复杂的数据模型,数组都扮演着核心角色。

与其他静态类型语言(如 Java, C++)中的数组不同,JavaScript 数组具有极大的灵活性:它们可以存储任何类型的数据(数字、字符串、对象、甚至其他数组),并且其长度是可变的,可以动态地添加或移除元素。这种动态性,结合 JavaScript 提供的丰富内置方法,使得数组成为处理各种数据集合的强大工具。

理解 JavaScript 数组的底层特性、创建方式、各种操作方法以及它们如何与函数式编程范式结合,是编写高效、可维护和健壮 JavaScript 代码的关键。

本教程将全面深入地探讨 JavaScript 数组的方方面面。我们将从定义数组及其动态、异构特性开始。随后,我们将详细剖析创建数组的各种方式,并深入学习如何访问、添加、移除和修改数组元素。我们将着重介绍一系列强大的数组迭代方法(如 forEach, map, filter, reduce),它们是现代 JavaScript 编程中不可或缺的工具。此外,我们还将涵盖数组的其他实用方法,如查找、排序、拼接、转换为字符串等,并讨论稀疏数组类数组对象以及数组操作的最佳实践和常见陷阱,帮助您彻底掌握 JavaScript 中的数组,编写出更高效、更具表现力的数据处理代码。


1. 什么是 JavaScript 数组?

在 JavaScript 中,数组是一个有序的列表,可以存储多个值。这些值被称为数组的元素 (elements),每个元素在数组中都有一个唯一的数字索引 (index),索引从 0 开始。

核心特性:

  • 动态大小: 数组的长度不是固定的,可以在运行时动态增长或缩短。
  • 异构性: 数组可以包含不同数据类型的元素,例如:[1, "hello", true, { name: "Alice" }]
  • 基于 0 的索引: 数组的第一个元素索引为 0,第二个为 1,以此类推。
  • 特殊对象: 数组本质上是特殊的对象。它们继承自 Array.prototype,并具有许多专门用于操作数组的方法和属性。
  • 按引用传递: 当将一个数组赋值给另一个变量或将其作为参数传递给函数时,实际上是传递了对同一个数组对象的引用。

2. 创建数组

JavaScript 提供了几种创建数组的方式,其中数组字面量是最常见和推荐的方式。

2.1 数组字面量 (Array Literal) - 推荐

这是创建数组最常用、最简洁的方式。使用方括号 [] 包裹元素,元素之间用逗号 , 分隔。

// 空数组
let emptyArray = [];

// 包含数字的数组
let numbers = [1, 2, 3, 4, 5];

// 包含字符串的数组
let fruits = ["Apple", "Banana", "Orange"];

// 包含不同数据类型的数组(异构数组)
let mixedArray = [10, "Hello", true, { name: "Alice" }, null];

// 嵌套数组(多维数组)
let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];
2.2 Array 构造函数

可以使用 new Array() 构造函数来创建数组,但通常不推荐,因为它在某些情况下可能导致混淆。

  • 指定长度: 如果只传入一个数字参数,它会创建一个指定长度的稀疏数组(数组中不包含任何元素,但 length 属性是该数字)。
    let arr1 = new Array(5); // 创建一个长度为 5 的稀疏数组,元素为 empty * 5
    console.log(arr1);       // [empty × 5]
    console.log(arr1.length); // 5
    
  • 指定元素: 如果传入多个参数,或者传入一个非数字参数,这些参数将成为数组的元素。
    let arr2 = new Array(1, 2, 3); // [1, 2, 3]
    let arr3 = new Array("Apple", "Banana"); // ["Apple", "Banana"]
    

不推荐 new Array(number) 形式,因为它与 new Array(element1, element2) 形式的语义冲突,容易产生误解。

2.3 Array.of() (ES6)

Array.of() 方法提供了一种更可靠的方式来创建数组,其行为始终一致:它将参数作为元素列表创建新数组,无论参数是数字还是其他类型。

let arr4 = Array.of(1, 2, 3);    // [1, 2, 3]
let arr5 = Array.of(7);          // [7] (而不是长度为 7 的稀疏数组)
let arr6 = Array.of("hello", "world"); // ["hello", "world"]

这是替代 new Array() 指定元素列表的更好选择。

2.4 Array.from() (ES6)

Array.from() 方法允许从一个类数组对象可迭代对象创建一个新的、浅拷贝的 Array 实例。

// 从字符串创建数组 (每个字符一个元素)
let strToArray = Array.from("Hello"); // ["H", "e", "l", "l", "o"]

// 从 Set (可迭代对象) 创建数组
let mySet = new Set([1, 2, 3]);
let setToArray = Array.from(mySet); // [1, 2, 3]

// 从 NodeList (类数组对象) 创建数组
// let divs = document.querySelectorAll('div');
// let divArray = Array.from(divs);

// 也可以传入一个映射函数
let numbersSquared = Array.from([1, 2, 3], x => x * x); // [1, 4, 9]

3. 访问和修改数组元素

3.1 访问元素

通过元素的索引来访问,索引从 0 开始。

let fruits = ["Apple", "Banana", "Orange"];
console.log(fruits[0]); // "Apple"
console.log(fruits[1]); // "Banana"
console.log(fruits[2]); // "Orange"
console.log(fruits[3]); // undefined (索引超出范围)
3.2 修改元素

通过索引直接赋值来修改元素。

let fruits = ["Apple", "Banana", "Orange"];
fruits[1] = "Grape";
console.log(fruits); // ["Apple", "Grape", "Orange"]
3.3 at(index) 方法 (ES2022)

at() 方法提供了一种更灵活的方式来访问数组元素,尤其适用于负数索引(从数组末尾开始计数)。

let colors = ["red", "green", "blue"];
console.log(colors.at(0));    // "red"
console.log(colors.at(2));    // "blue"
console.log(colors.at(-1));   // "blue" (最后一个元素)
console.log(colors.at(-2));   // "green" (倒数第二个元素)
console.log(colors.at(5));    // undefined

4. 数组的长度 length 属性

length 属性返回数组中元素的数量。它是可读可写的。

let items = ["a", "b", "c"];
console.log(items.length); // 3

// 通过修改 length 属性来截断数组
items.length = 2;
console.log(items);        // ["a", "b"]
items.length = 0;
console.log(items);        // [] (清空数组)

// 增大 length 属性会创建稀疏数组
items.length = 5;
console.log(items);        // [empty × 5]

5. 添加和移除元素

5.1 push()pop() (操作数组末尾)
  • push(...elements) 在数组末尾添加一个或多个元素,并返回新数组的长度。
    let fruits = ["Apple", "Banana"];
    fruits.push("Orange", "Grape");
    console.log(fruits); // ["Apple", "Banana", "Orange", "Grape"]
    
  • pop() 移除并返回数组的最后一个元素。
    let fruits = ["Apple", "Banana", "Orange"];
    let lastFruit = fruits.pop();
    console.log(lastFruit); // "Orange"
    console.log(fruits);    // ["Apple", "Banana"]
    
5.2 unshift()shift() (操作数组开头)
  • unshift(...elements) 在数组开头添加一个或多个元素,并返回新数组的长度。
    let fruits = ["Banana", "Orange"];
    fruits.unshift("Apple", "Kiwi");
    console.log(fruits); // ["Apple", "Kiwi", "Banana", "Orange"]
    
  • shift() 移除并返回数组的第一个元素。
    let fruits = ["Apple", "Banana", "Orange"];
    let firstFruit = fruits.shift();
    console.log(firstFruit); // "Apple"
    console.log(fruits);     // ["Banana", "Orange"]
    
5.3 splice(startIndex, deleteCount, ...itemsToAdd)

splice() 是一个非常强大的方法,可以添加、删除或替换数组中的元素。

  • startIndex:开始修改的位置(必需)。
  • deleteCount:要删除的元素数量(可选)。如果为 0,则不删除元素。
  • itemsToAdd:要添加的元素(可选)。
  • 返回一个包含被删除元素的数组(如果没有删除元素,则返回空数组)。
let colors = ["red", "green", "blue", "yellow"];

// 删除元素
colors.splice(1, 2); // 从索引1开始删除2个元素 ("green", "blue")
console.log(colors); // ["red", "yellow"]

// 添加元素
colors.splice(1, 0, "orange", "purple"); // 从索引1开始删除0个元素,添加"orange", "purple"
console.log(colors); // ["red", "orange", "purple", "yellow"]

// 替换元素
colors.splice(1, 2, "pink"); // 从索引1开始删除2个元素 ("orange", "purple"),添加"pink"
console.log(colors); // ["red", "pink", "yellow"]

6. 数组的迭代

6.1 for 循环

最传统的迭代方式。

let arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}
6.2 for...of 循环 (ES6)

遍历可迭代对象(包括数组)的值。

let arr = ["a", "b", "c"];
for (let element of arr) {
  console.log(element);
}
6.3 forEach(callback(currentValue, index, array))

对数组的每个元素执行一次提供的函数。它不返回新数组,只用于执行副作用。

let numbers = [1, 2, 3];
numbers.forEach(function(num, index) {
  console.log(`Element at index ${index} is: ${num}`);
});
// Output:
// Element at index 0 is: 1
// Element at index 1 is: 2
// Element at index 2 is: 3
6.4 map(callback(currentValue, index, array))

创建一个新数组,其结果是调用提供的函数对每个元素进行操作后的返回值。它不修改原数组。

let numbers = [1, 2, 3];
let doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] (原数组未改变)
6.5 filter(callback(currentValue, index, array))

创建一个新数组,其中包含所有通过提供的函数测试的元素。它不修改原数组。

let numbers = [1, 2, 3, 4, 5];
let evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]
console.log(numbers); // [1, 2, 3, 4, 5] (原数组未改变)
6.6 reduce(callback(accumulator, currentValue, index, array), initialValue)

对数组中的每个元素执行一个 reducer 函数(从左到右),将其结果汇总为单个返回值。

  • accumulator:累加器,保存上次回调函数返回的值。
  • currentValue:当前处理的数组元素。
  • initialValue (可选):累加器的初始值。如果未提供,则 accumulator 的初始值为数组的第一个元素,currentValue 从第二个元素开始。
let numbers = [1, 2, 3, 4];
// 计算和
let sum = numbers.reduce((acc, num) => acc + num, 0); // 0 是 initialValue
console.log(sum); // 10

// 扁平化数组数组
let nestedArr = [[1, 2], [3, 4], [5, 6]];
let flattened = nestedArr.reduce((acc, currentArr) => acc.concat(currentArr), []);
console.log(flattened); // [1, 2, 3, 4, 5, 6]

7. 更多常用数组方法

7.1 查找和测试
  • find(callback) (ES6): 返回数组中满足提供的测试函数的第一个元素的值。如果没有找到,则返回 undefined
    let users = [{id: 1, name: "A"}, {id: 2, name: "B"}];
    let user = users.find(u => u.id === 2);
    console.log(user); // {id: 2, name: "B"}
    
  • findIndex(callback) (ES6): 返回数组中满足提供的测试函数的第一个元素的索引。如果没有找到,则返回 -1
    let users = [{id: 1, name: "A"}, {id: 2, name: "B"}];
    let index = users.findIndex(u => u.id === 2);
    console.log(index); // 1
    
  • includes(value, [fromIndex]) (ES6): 判断数组是否包含某个元素,返回 truefalse
    let fruits = ["Apple", "Banana"];
    console.log(fruits.includes("Banana")); // true
    console.log(fruits.includes("Kiwi"));   // false
    
  • some(callback) 测试数组中是否至少有一个元素通过了提供的函数测试。返回 truefalse
    let numbers = [1, 3, 5, 7];
    let hasEven = numbers.some(num => num % 2 === 0);
    console.log(hasEven); // false
    
  • every(callback) 测试数组中的所有元素是否都通过了提供的函数测试。返回 truefalse
    let numbers = [2, 4, 6, 8];
    let allEven = numbers.every(num => num % 2 === 0);
    console.log(allEven); // true
    
  • indexOf(searchValue, [fromIndex]) 返回 searchValue 第一次出现的索引,未找到返回 -1
  • lastIndexOf(searchValue, [fromIndex]) 返回 searchValue 最后一次出现的索引,未找到返回 -1
7.2 排序和反转
  • sort([compareFunction]) 对数组元素进行原地排序。默认按字符串 Unicode 码点顺序排序。
    • compareFunction (可选):用于指定排序顺序的函数。
    let numbers = [3, 1, 4, 1, 5, 9];
    numbers.sort(); // 默认按字符串排序:[1, 1, 3, 4, 5, 9]
    console.log(numbers);
    
    // 数字排序 (从小到大)
    numbers.sort((a, b) => a - b);
    console.log(numbers); // [1, 1, 3, 4, 5, 9]
    
    // 数字排序 (从大到小)
    numbers.sort((a, b) => b - a);
    console.log(numbers); // [9, 5, 4, 3, 1, 1]
    
    注意:sort() 会修改原数组。
  • reverse() 反转数组中元素的顺序。它会修改原数组。
    let arr = [1, 2, 3];
    arr.reverse();
    console.log(arr); // [3, 2, 1]
    
7.3 拼接和合并
  • concat(...arraysOrValues) 合并两个或多个数组。它不修改原数组,返回一个新数组。
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let combined = arr1.concat(arr2, 5, [6, 7]);
    console.log(combined); // [1, 2, 3, 4, 5, 6, 7]
    console.log(arr1);     // [1, 2] (原数组未改变)
    
  • 扩展运算符 (...) (ES6): 也可以用于合并数组,且常用于创建数组的浅拷贝。
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let combined = [...arr1, ...arr2]; // [1, 2, 3, 4]
    let copy = [...arr1];               // [1, 2] (浅拷贝)
    
7.4 转换字符串
  • join(separator) 将数组中的所有元素连接成一个字符串。
    • separator (可选):用于分隔每个元素的字符串。默认是逗号 ,
    let fruits = ["Apple", "Banana", "Orange"];
    console.log(fruits.join());    // "Apple,Banana,Orange"
    console.log(fruits.join("-")); // "Apple-Banana-Orange"
    

8. 稀疏数组 (Sparse Arrays)

当数组的某些索引没有对应的元素时,就是稀疏数组。
这通常发生在:

  • 使用 new Array(length) 创建。
  • 直接为超出当前 length 的索引赋值。
  • 使用 delete 运算符删除数组元素。
let sparseArr = [1, , 3]; // 索引1处是空的
console.log(sparseArr.length); // 3
console.log(sparseArr[1]);     // undefined

let arr = [1, 2, 3];
delete arr[1]; // 元素被删除,但索引位置仍然存在 (变为 empty)
console.log(arr); // [1, empty, 3]
console.log(arr.length); // 3

迭代稀疏数组时,for 循环会访问到 undefined,而 forEachmapfilter 等迭代方法会跳过空的索引。

9. 类数组对象 (Array-like Objects)

类数组对象是具有 length 属性和索引访问元素的能力的对象,但它们不具备 Array.prototype 上的方法(如 push, pop, forEach 等)。
常见类数组对象:

  • arguments 对象(函数内部)
  • DOM NodeList(例如 document.querySelectorAll() 的返回值)
  • HTMLCollection
function sumAll() {
  console.log(arguments); // arguments 对象是一个类数组对象
  // arguments.forEach(...) 会报错
  let args = Array.from(arguments); // 转换为真正的数组
  console.log(args.reduce((acc, val) => acc + val, 0));
}
sumAll(1, 2, 3); // 6

将类数组对象转换为数组:

  • Array.from(arrayLikeObject) (推荐)
  • 扩展运算符:[...arrayLikeObject]
  • Array.prototype.slice.call(arrayLikeObject) (老式方法)

10. 最佳实践与常见陷阱

  • 优先使用数组字面量 [] 它更简洁,且避免了 new Array(number) 可能带来的混淆。
  • 理解可变与不可变方法:
    • 修改原数组 (Mutable): push, pop, shift, unshift, splice, sort, reverse, fill
    • 返回新数组 (Immutable): map, filter, reduce, slice, concat, Array.from(), 扩展运算符 [...]
      在函数式编程和 React/Vue 等框架中,通常推荐使用不可变操作来避免副作用和提高可预测性。
  • const 声明数组: const 声明的数组本身不能被重新赋值,但数组内部的元素可以被修改(因为 const 约束的是变量的引用,而不是引用指向的对象内容)。
    const myArr = [1, 2];
    myArr.push(3);       // 有效,myArr 现在是 [1, 2, 3]
    // myArr = [4, 5];   // 错误,不能重新赋值
    
  • 避免直接操作 length 属性来清空或截断数组: 这可能导致意想不到的结果(如稀疏数组)。使用 arr = [] (如果 arrlet 声明的) 或 arr.splice(0) 来清空,使用 arr.slice(0, newLength)arr.splice(newLength) 来截断。
  • 深拷贝与浅拷贝:
    • slice(), concat(), ... 运算符都创建数组的浅拷贝。如果数组元素是原始类型,这通常没问题。但如果元素是对象,拷贝的数组和原数组将共享对这些对象的引用。
    • 对于深拷贝,需要使用 JSON.parse(JSON.stringify(arr)) (有局限性,如不能拷贝函数、undefined 等) 或专门的深拷贝库 (如 Lodash 的 cloneDeep)。
  • 迭代器性能: 对于简单的迭代,for 循环通常是最快的。但对于需要转换或过滤的场景,map, filter, reduce 等方法更具可读性和表达力。

11. 总结与展望

JavaScript 数组是其核心数据结构之一,其灵活性和丰富的内置方法使其成为处理有序数据集合的强大工具。

  • 核心: 动态、异构、基于 0 索引的对象。
  • 创建: 推荐使用数组字面量 []
  • 操作: push, pop, shift, unshift, splice 进行增删改。
  • 迭代: forEach, map, filter, reduce 等是现代 JavaScript 中强大的数据处理工具。
  • Immutable vs. Mutable: 理解哪些方法会修改原数组,哪些会返回新数组,对于编写健壮代码至关重要。
  • BigInt 专门用于表示和操作任意大的整数,解决了 Number 类型的整数精度限制。

通过深入理解 JavaScript 数组的这些特性和方法,你将能够更自信、更高效地管理和操作数据集合,编写出更健壮、更可靠的 JavaScript 应用程序。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐