`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
  • このコードでは、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)

配列を順に処理する方法はいくつかあり、用途や書き方の好みによって使い分けることができます。ここでは代表的な 3 種類のループ構文を紹介します。

 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 は関数型スタイルで記述でき、ログ出力やデータ更新など「各要素に対して何かを行う」といった副作用を伴う処理にも向いています。ただし 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 が前、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 ] ]
  • このコードは配列を 1 階層だけフラット化した結果を示したものです。
  • flat は階層の深さを指定できるため、必要に応じて柔軟にネストを解消できます。

flatMap

flatMapメソッドは、各要素に変換処理を行ったうえで、その結果を自動的に1次元の配列へ平坦化してくれる便利なメソッドです。

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 は元の配列を変更しない非破壊的メソッドです。用途に応じて使い分けることが大切です。
  • forEachawait を用いた非同期処理のループには向いていません。非同期処理を順番に確実に実行したい場合は、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