`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 屬性取得長度。
  • 陣列文字是最常見且易讀的寫法,在日常情境中最常被使用。

在尾端或開頭新增與移除元素

陣列可以輕鬆地在尾端或開頭新增或移除元素。這些操作對於實作堆疊(stack)或佇列(queue)這類結構也很有用。

 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 或展開語法(spread syntax)等方式複製陣列時,產生的是淺層複製。如果陣列中含有物件,可能會因修改新陣列而意外改變原始資料,請小心。
  • 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