`Array` 对象

`Array` 对象

本文介绍了Array对象。

我将以简单易懂的方式,逐步讲解数组的实际用法。

YouTube Video

Array 对象

JavaScript中的Array对象是各种数据处理基础中最重要的结构之一。从基础的数组操作到高阶函数等高效数据转换工具,有许多特性值得了解。

数组基础

在JavaScript中,数组是用于一起处理多个值的基本数据结构。这里,我们通过简单示例介绍如何创建数组,以及如何读写其元素。

 1// Create arrays in different ways
 2const arr1 = [1, 2, 3];           // array literal
 3const arr2 = new Array(4, 5, 6);  // Array constructor
 4const arr3 = Array.of(7, 8, 9);   // Array.of
 5
 6console.log("arr1 created with literal.   :", arr1);
 7console.log("arr2 created with constructor:", arr2);
 8console.log("arr3 created with Array.of.  :", arr3);
 9// arr1 created with literal.   : [ 1, 2, 3 ]
10// arr2 created with constructor: [ 4, 5, 6 ]
11// arr3 created with Array.of.  : [ 7, 8, 9 ]
12
13// Access and modify elements
14let first = arr1[0];              // read element
15console.log("First element of arr1:", first);
16// First element of arr1: 1
17
18arr1[1] = 20;                     // modify element
19console.log("arr1 after modifying index 1:", arr1);
20// arr1 after modifying index 1: [ 1, 20, 3 ]
21
22const len = arr1.length;          // get length
23console.log("Length of arr1:", len);
24// Length of arr1: 3
  • 下面的代码展示了三种创建数组的方法,如何使用索引读取和更新元素,以及如何通过length属性获取长度。
  • 数组字面量是最常用且可读性最高的,在日常情况下使用最频繁。

添加和移除元素(在末尾或开头)

数组允许你轻松地在末尾或开头添加或移除元素。这些操作在实现栈或队列等结构时也非常有用。

 1// Push and pop (stack-like)
 2const stack = [];
 3console.log("Initial stack:", stack);
 4
 5stack.push(1);                    // push 1
 6console.log("After push(1):", stack);
 7
 8stack.push(2);                    // push 2
 9console.log("After push(2):", stack);
10
11const last = stack.pop();         // pop -> 2
12console.log("Popped value:", last);
13console.log("Stack after pop():", stack);
14
15// Unshift and shift (queue-like)
16const queue = [];
17console.log("Initial queue:", queue);
18
19queue.push('a');                  // add to end
20console.log("After push('a'):", queue);
21
22queue.unshift('start');           // add to front
23console.log("After unshift('start'):", queue);
24
25const firstItem = queue.shift();  // remove from front
26console.log("Shifted value:", firstItem);
27console.log("Queue after shift():", queue);
28
29// Initial stack: []
30// After push(1): [ 1 ]
31// After push(2): [ 1, 2 ]
32// Popped value: 2
33// Stack after pop(): [ 1 ]
34
35// Initial queue: []
36// After push('a'): [ 'a' ]
37// After unshift('start'): [ 'start', 'a' ]
38// Shifted value: start
39// Queue after shift(): [ 'a' ]
  • pushpop操作于数组的末尾。它们非常适合实现栈结构。
  • unshiftshift操作于数组的开头。但需要注意,操作开头时会导致所有元素的索引发生内部移动,因此开头操作开销较大。

处理中间元素(splice 和 slice)

在处理中间元素时,根据是否需要修改原数组选择使用spliceslice。如果你只是想提取数组的一部分,请使用 slice;如果你想修改数组本身,比如插入或删除元素,请使用 splice

 1// slice (non-destructive)
 2const nums = [0, 1, 2, 3, 4];
 3console.log("Original nums:", nums);
 4
 5const part = nums.slice(1, 4); // returns [1, 2, 3]
 6console.log("Result of nums.slice(1, 4):", part);
 7console.log("nums after slice (unchanged):", nums);
 8
 9// splice (destructive)
10const arr = [10, 20, 30, 40];
11console.log("\nOriginal arr:", arr);
12
13// remove 1 item at index 2, insert 25 and 27
14arr.splice(2, 1, 25, 27);
15console.log("After arr.splice(2, 1, 25, 27):", arr);
16
17// Original nums: [ 0, 1, 2, 3, 4 ]
18// Result of nums.slice(1, 4): [ 1, 2, 3 ]
19// nums after slice (unchanged): [ 0, 1, 2, 3, 4 ]
20
21// Original arr: [ 10, 20, 30, 40 ]
22// After arr.splice(2, 1, 25, 27): [ 10, 20, 25, 27, 40 ]
  • slice只提取元素,不会修改原数组。
  • splice添加或移除元素并会直接修改数组本身,因此要注意其带来的影响。

遍历(for / for...of / forEach)

有多种方式可以按顺序处理数组,可根据用途和代码风格进行选择。以下介绍三种典型的循环结构。

 1const items = ['apple', 'banana', 'cherry'];
 2console.log("Items:", items);
 3
 4// classic for
 5console.log("\n--- Classic for loop ---");
 6for (let i = 0; i < items.length; i++) {
 7  console.log(`Index: ${i}, Value: ${items[i]}`);
 8}
 9
10// for...of (recommended for values)
11console.log("\n--- for...of loop ---");
12for (const item of items) {
13  console.log(`Value: ${item}`);
14}
15
16// forEach (functional style)
17console.log("\n--- forEach loop ---");
18items.forEach((item, index) => {
19  console.log(`Index: ${index}, Value: ${item}`);
20});
  • for循环最为灵活,可以对索引进行操作,并通过break等语句精细控制遍历过程。
  • for...of语法简洁,便于处理元素值,并兼顾可读性。
  • forEach可以编写函数式代码,适合执行副作用操作,比如记录日志或为每个元素更新数据。但请注意,forEach不能使用breakcontinue不适合与await一起进行异步处理

map / filter / reduce — 高阶函数

mapfilterreduce 是在转换、过滤或聚合数组时经常使用的高阶函数。由于可以清晰地表达重复处理,您的代码变得简单且易于理解。

 1const numbers = [1, 2, 3, 4, 5];
 2console.log("Original numbers:", numbers);
 3
 4// map: transform each item
 5const doubled = numbers.map(n => n * 2);
 6console.log("\nResult of map (n * 2):", doubled);
 7
 8// filter: select items
 9const evens = numbers.filter(n => n % 2 === 0);
10console.log("Result of filter (even numbers):", evens);
11
12// reduce: accumulate to single value
13const sum = numbers.reduce((acc, n) => acc + n, 0);
14console.log("Result of reduce (sum):", sum);
15
16// Original numbers: [ 1, 2, 3, 4, 5 ]
17// Result of map (n * 2): [ 2, 4, 6, 8, 10 ]
18// Result of filter (even numbers): [ 2, 4 ]
19// Result of reduce (sum): 15
  • 这些方法让你以声明式风格专注于处理目标,提高可读性并减少不必要的副作用。

find / findIndex / some / every

下面简要介绍数组的查找和条件检查方法。这些方法有助于查找符合条件的元素或对集合进行布尔判断。

 1const users = [
 2  { id: 1, name: 'Alice' },
 3  { id: 2, name: 'Bob' },
 4  { id: 3, name: 'Carol' }
 5];
 6
 7console.log("Users:", users);
 8
 9// Find the first user whose name is 'Bob'
10const bob = users.find(user => user.name === 'Bob');
11console.log("\nResult of find (name === 'Bob'):", bob);
12
13// Find index of the user whose id is 3
14const indexOfId3 = users.findIndex(user => user.id === 3);
15console.log("Result of findIndex (id === 3):", indexOfId3);
16
17// Check if there exists a user with id = 2
18const hasId2 = users.some(user => user.id === 2);
19console.log("Result of some (id === 2):", hasId2);
20
21// Check if all users have a numeric id
22const allHaveNumericId = users.every(user => typeof user.id === 'number');
23console.log("Result of every (id is number):", allHaveNumericId);
24// Result of find (name === 'Bob'): { id: 2, name: 'Bob' }
25// Result of findIndex (id === 3): 2
26// Result of some (id === 2): true
27// Result of every (id is number): true
  • find 返回第一个符合条件的元素。
  • findIndex 返回第一个匹配条件的元素索引。
  • some 只要有一个元素满足条件就返回true
  • every 只有全部元素都满足条件才返回true

这些方法在数组处理时都非常实用,按需使用能让代码简洁明了。

排序与比较函数

数组通过sort方法排序,但默认以字符串方式进行比较,这会导致数字排序结果不如预期。

 1const nums = [10, 2, 33, 4];
 2console.log("Original nums:", nums);
 3
 4// Default sort: compares elements as strings (not suitable for numbers)
 5nums.sort();
 6console.log("\nAfter default sort (string comparison):", nums);
 7
 8// Numeric ascending sort using a compare function
 9nums.sort((a, b) => a - b);
10console.log("After numeric sort (a - b):", nums);
11
12// Sort objects by a property
13const people = [{ age: 30 }, { age: 20 }, { age: 25 }];
14console.log("\nOriginal people:", people);
15
16people.sort((a, b) => a.age - b.age);
17console.log("After sorting people by age:", people);
18
19// After default sort (string comparison): [ 10, 2, 33, 4 ]
20// After numeric sort (a - b): [ 2, 4, 10, 33 ]
21// Original people: [ { age: 30 }, { age: 20 }, { age: 25 } ]
22// After sorting people by age: [ { age: 20 }, { age: 25 }, { age: 30 } ]
  • 在对数字或对象排序时,一定要指定比较函数以确保排序结果符合预期。
  • 在比较函数中,返回负值则将a排在b前面,返回正值则b排在a前面,返回0则顺序不变。

数组复制与不可变性

复制数组时,理解“引用复制”和“浅拷贝”的区别很重要。特别要注意,若数组中包含对象,浅复制会导致内部对象被共享。

引用复制

当您将一个数组赋值给另一个变量时,数组本身并不会被复制,而是复制了指向同一个数组的“引用”。

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Reference copy; modifying b also affects a
 5const b = a;
 6console.log("\nReference copy b = a:", b);
 7// Reference copy b = a: [ 1, 2, 3 ]
 8
 9b[0] = 100;
10console.log("After modifying b[0] = 100:");
11console.log("a:", a);  // a: [ 100, 2, 3 ] (affected)
12console.log("b:", b);  // b: [ 100, 2, 3 ]
  • 使用引用复制时,如果您通过复制的变量修改数组内容,这些更改也会反映在引用同一数组的原始变量上。

浅拷贝

使用 slice() 或扩展语法(...)会创建“浅拷贝”,因为仅复制了数值,原数组和复制数组被视为独立的实体。

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Shallow copy (new array)
 5const b = a.slice();
 6console.log("\nShallow copy using slice():", b);
 7// Shallow copy using slice(): [ 100, 2, 3 ]
 8
 9const c = [...a];
10console.log("Shallow copy using spread [...a]:", c);
11// Shallow copy using spread [...a]: [ 100, 2, 3 ]
12
13// Modifying c or d does NOT affect a
14b[1] = 200;
15c[2] = 300;
16console.log("\nAfter modifying b[1] = 200 and c[2] = 300:");
17console.log("a:", a);   // [ 100, 2, 3 ]
18console.log("b:", b);   // [ 100, 200, 3 ]
19console.log("c:", c);   // [ 100, 2, 300 ]
  • 此代码演示了通过 slice() 或扩展语法创建的数组浅拷贝不会影响原始数组。

浅拷贝与不可变性

即使您使用“浅拷贝”复制数组,如果数组中包含对象,也有可能发生未预期的共享。

 1// Shallow copy doesn't clone inner objects
 2const nested = [{ x: 1 }, { x: 2 }];
 3console.log("\nOriginal nested:", nested);
 4// Original nested: [ { x: 1 }, { x: 2 } ]
 5
 6const shallow = nested.slice();
 7console.log("Shallow copy of nested:", shallow);
 8// Shallow copy of nested: [ { x: 1 }, { x: 2 } ]
 9
10// Changing inner object affects both arrays
11shallow[0].x = 99;
12console.log("\nAfter shallow[0].x = 99:");
13console.log("nested:", nested);
14console.log("shallow:", shallow);
15// nested: [ { x: 99 }, { x: 2 } ]
16// shallow: [ { x: 99 }, { x: 2 } ]
  • 此代码演示了浅拷贝时,内部对象是共享的,因此修改这些内部对象会影响原数组和复制数组。
  • 如需独立数据,需进行“深复制”,比如使用structuredClone()或JSON转换。

实用工具方法

以下为数组操作中常用的工具方法。根据目标合理使用这些工具方法,可编写简洁易读的代码。

includes

includes 方法用于检查数组中是否包含某个特定的值。

1// includes: check if array contains a value
2const letters = ['a', 'b', 'c'];
3const hasB = letters.includes('b');
4console.log("letters:", letters);  // [ 'a', 'b', 'c' ]
5console.log("letters.includes('b'):", hasB); // true
  • 在此代码中,includes 方法用于简洁地判断数组中是否存在指定的值。

concat

concat 方法生成一个新数组,并将指定的数组或数值追加到末尾,同时不会改变原始数组。

1// concat: merge arrays without mutation
2const a1 = [1, 2];
3const a2 = [3, 4];
4const combined = a1.concat(a2);
5console.log("a1.concat(a2):", combined); // [ 1, 2, 3, 4 ]
6console.log("a1(unchanged):", a1);       // [ 1, 2 ]
  • 此代码表明 concat 是非破坏性的,允许在保留原数组的同时生成新数组。

flat

使用 flat 方法可以将嵌套数组扁平化。

1// flat and flatMap: flatten arrays or map + flatten in one step
2const nested = [1, [2, [3]]];
3console.log("nested:", nested); // nested: [ 1, [ 2, [ 3 ] ] ]
4console.log("nested.flat():", nested.flat()); // default depth = 1
5// nested.flat(): [ 1, 2, [ 3 ] ]
  • 此代码演示了数组扁平化一级后的结果。
  • 由于 flat 允许您指定深度,可以根据需要灵活地解决嵌套问题。

flatMap

flatMap 方法对每个元素进行转换,然后自动将结果展平成一维数组。

1// flat and flatMap: flatten arrays or map + flatten in one step
2const words = ['hello world', 'hi'];
3console.log("words:", words); // [ 'hello world', 'hi' ]
4
5const splitWords = words.flatMap(w => w.split(' '));
6console.log("words.flatMap(w => w.split(' ')):", splitWords);
7// words.flatMap(w => w.split(' ')): [ 'hello', 'world', 'hi' ]
  • 此代码展示了如何将数组中的每个字符串按空格分割,并将结果合并并展平成一个新数组的示例。

join

join 方法会用指定的分隔符将数组元素连接起来生成一个字符串。

1// join: combine elements into a string with a separator
2const csv = ['a', 'b', 'c'].join(',');
3console.log("['a', 'b', 'c'].join(','):", csv);  // a,b,c
  • 在这段代码中,join 方法被用于将数组转换为用逗号分隔的字符串。

常见陷阱

数组操作表面看似简单,其中存在许多容易导致非预期行为的细节。日常数组操作容易忽略这些陷阱,牢记下面这些问题能极大提升代码的可靠性。

  • Arraysort()方法默认按字符串进行比较排序。如需正确排序数字,务必传入比较函数。
  • 通过slice或扩展运算符等复制数组时,会得到浅复制。如数组中包含对象,需注意原始数据可能会被意外修改。
  • splice破坏性方法,会直接修改数组,而slice非破坏性方法,不会修改原数组。根据实际需求合理区分使用非常重要。
  • forEach不适合与await异步处理结合的循环。如果需要可靠顺序地执行异步操作,推荐使用for...of

实际范例

以下是一个综合应用数组方法的例子:'从用户数据中获取总年龄,筛选年龄大于等于30的人,并生成姓名列表。'。

 1const users = [
 2  { name: 'Alice', age: 28 },
 3  { name: 'Bob', age: 34 },
 4  { name: 'Carol', age: 41 },
 5  { name: 'Dave', age: 19 }
 6];
 7
 8console.log("Users:", users);
 9
10// sum ages
11const totalAge = users.reduce((acc, u) => acc + u.age, 0);
12console.log("\nTotal age of all users:", totalAge);
13// Total age of all users: 122
14
15// filter and map names of users 30 and older
16const namesOver30 = users
17  .filter(u => u.age >= 30)
18  .map(u => u.name);
19
20console.log("Users aged 30 or older (names):", namesOver30);
21// Users aged 30 or older (names): [ 'Bob', 'Carol' ]
  • 通过链式使用reducefiltermap,即可简单实现数据聚合、条件筛选和转换。
  • 这样的“数据处理管道”可读性强,副作用少,常用于实际开发。

总结

JavaScript数组不仅基础操作应用广泛,通过高阶函数还能让代码更简洁、更具表现力。虽然要理解的细节很多,但熟练掌握各种用法后,数据处理会变得更加顺畅。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video