JavaScriptの配列は非常に柔軟で強力なデータ構造ですが、複数の情報セットを扱う際には「2次元配列」が非常に役立ちます。表形式のデータや座標、ゲームのマップなど、様々な場面でその真価を発揮します。本記事では、JavaScriptで2次元配列を宣言する方法から、要素の追加、ソート、さらには3次元配列の概念、そしてES6以降の便利な機能まで、幅広く解説していきます。

なお、本記事でご紹介するJavaScriptの2次元配列に関する情報は、一般的なプログラミングの概念とJavaScriptの標準的な仕様に基づいて解説しています。現在、特定の政府機関や公的機関から、JavaScriptの2次元配列の操作に関する直接的な技術解説は提供されておりません。

JavaScriptで2次元配列を宣言する方法

1. 配列リテラルによる基本的な宣言

JavaScriptで2次元配列を宣言する最も一般的で直感的な方法は、配列リテラル([])を使用することです。これは、配列の中にさらに配列をネストして記述する方法で、視覚的にもデータの構造を理解しやすいため、多くの開発者に利用されています。たとえば、学校のクラスの生徒名と点数を管理する場合や、カレンダーの週と曜日を表現する場合などに適しています。

基本的な構文は、外側の配列が「行」を表し、内側の配列が「列」の要素を持つと考えると理解しやすいでしょう。以下の例では、3行3列の簡単な数値データを持つ2次元配列を宣言しています。


const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];
console.log(matrix[0][0]); // 出力: 1
console.log(matrix[1][2]); // 出力: 6

この方法では、宣言と同時に初期値を設定できるため、固定されたデータ構造を扱う場合に非常に効率的です。また、各内側の配列(行)の長さが異なっていても問題なく動作します。

2. Arrayコンストラクタを用いた宣言

JavaScriptのArrayコンストラクタを使用すると、特定のサイズの配列を事前に作成することができます。これは、特に配列のサイズがあらかじめ決まっているが、まだ具体的な要素が未定である場合に便利です。ただし、2次元配列を作成する際には、単にnew Array(rows).fill(new Array(cols))とするだけでは期待通りの結果にならない場合があるので注意が必要です。fill()メソッドは同じオブジェクトへの参照を埋め込んでしまうため、すべての内側の配列が同じ配列を参照してしまい、一つを変更すると全てが変更されてしまうという問題が発生します。

この問題を回避するためには、map()メソッドと組み合わせるのが一般的です。


const rows = 3;
const cols = 4;
const grid = Array(rows).fill(null).map(() => Array(cols).fill(0));
console.log(grid);
/*
出力:
[
  [0, 0, 0, 0],
  [0, 0, 0, 0],
  [0, 0, 0, 0]
]
*/
grid[0][0] = 10;
console.log(grid[0][0]); // 出力: 10

このように、fill(null)で一度ダミーの要素を埋め、その後map()で新しい配列を生成することで、それぞれの内側の配列が独立したオブジェクトとして扱われます。このアプローチは、動的なサイズを持つ2次元配列を生成する際に特に有用です。

3. 初期値を持つ2次元配列の作成

上記で説明した配列リテラルやArrayコンストラクタを用いた方法に加えて、より複雑な初期値を持つ2次元配列を作成することも可能です。特に、各要素がオブジェクトである場合や、行ごとに異なるデータ構造を持つ場合などです。例えば、ユーザーの属性情報を格納する2次元配列を考えてみましょう。各行がユーザーを表し、各要素がそのユーザーのidnamescoreといったプロパティを持つオブジェクトである場合です。


const usersData = [
  { id: 1, name: 'Alice', score: 95 },
  { id: 2, name: 'Bob', score: 88 },
  { id: 3, name: 'Charlie', score: 72 }
];

console.log(usersData[0].name); // 出力: Alice
console.log(usersData[2].score); // 出力: 72

この例では、実際には配列の配列ではなく、「オブジェクトの配列」としてデータを表現していますが、これは論理的には2次元配列と同様に、行と列(オブジェクトのプロパティ)でデータを管理する構造と見なせます。JavaScriptでは、このように柔軟なデータ構造を扱うことができ、タスクに応じて最適な表現方法を選択することが重要です。また、初期値を動的に生成する必要がある場合は、ループ処理や高階関数(mapなど)を組み合わせて利用することもできます。

空の2次元配列の作成と要素の追加(push)

1. 空の配列の初期化と`push`による行の追加

プログラムの実行中に動的にデータが増えていく場合、最初に空の2次元配列を宣言し、後から要素を追加していく方法が一般的です。最も基本的な初期化は、外側の配列だけを空で宣言する方法です。その後、新しい「行」を追加する際には、Array.prototype.push()メソッドを使用して、新しい配列を既存の2次元配列の末尾に加えます。

例えば、ゲームの盤面を動的に構築する場合や、ユーザーが入力したデータを順次追加していくようなシナリオでこの方法が役立ちます。以下は、空の2次元配列に新しい行(内側の配列)を追加する例です。


const dynamicGrid = []; // 空の2次元配列を初期化

// 新しい行(内側の配列)を追加
dynamicGrid.push([10, 20, 30]);
dynamicGrid.push([40, 50, 60]);
dynamicGrid.push([70, 80, 90]);

console.log(dynamicGrid);
/*
出力:
[
  [10, 20, 30],
  [40, 50, 60],
  [70, 80, 90]
]
*/

このアプローチは非常にシンプルで、配列の末尾に効率的に要素を追加できるため、動的に成長するデータ構造を扱う際に頻繁に利用されます。

2. `push`を使った列(要素)の追加

既存の2次元配列に新しい行を追加するだけでなく、既存の行に新しい列(要素)を追加することも頻繁に発生します。これは、特定のデータセットに新しい属性や情報が追加された場合などに有効です。この場合も、push()メソッドを使用しますが、対象となるのは外側の配列ではなく、追加したい要素が含まれる「内側の配列」です。

例えば、すでに存在するユーザーデータに新しい評価スコアを追加する場合を考えてみましょう。


const studentScores = [
  ['Alice', 85, 92],
  ['Bob', 78, 88],
  ['Charlie', 90, 75]
];

// Aliceのデータに新しいスコアを追加
studentScores[0].push(95);

// Bobのデータに新しいスコアを追加
studentScores[1].push(80);

console.log(studentScores);
/*
出力:
[
  ['Alice', 85, 92, 95],
  ['Bob', 78, 88, 80],
  ['Charlie', 90, 75]
]
*/

このように、インデックスを指定して内側の配列にアクセスし、その配列に対してpush()を実行することで、特定の行にのみ新しい列を追加できます。これにより、各行が異なる数の列を持つ「不規則な」2次元配列を作成することも可能です。

3. 既存の2次元配列への効率的な要素追加

大規模なデータセットやパフォーマンスが求められるアプリケーションにおいて、効率的な要素追加は重要です。push()メソッドは通常非常に効率的ですが、特定の状況では他のアプローチも検討する価値があります。例えば、配列の途中に要素を挿入する必要がある場合は、splice()メソッドが利用できます。ただし、splice()は既存の要素を移動させるため、大規模な配列ではパフォーマンスに影響を与える可能性があります。

また、新しい2次元配列を生成し、既存のデータと新しいデータを組み合わせて「マージ」するような操作も考えられます。これは、例えばスプレッド構文(...)とmap()などを利用して、既存のデータを変更せずに新しいデータセットを作成する際に有効です。


const oldData = [
  [1, 2],
  [3, 4]
];
const newDataRow = [5, 6];

// 新しい行を追加した新しい2次元配列を作成
const updatedData = [...oldData, newDataRow];

console.log(updatedData);
/*
出力:
[
  [1, 2],
  [3, 4],
  [5, 6]
]
*/

さらに、特定の行の途中に新しい要素を挿入する場合は、スプレッド構文とsplice()を組み合わせることも可能です。しかし、多くの場合、配列の末尾に要素を追加するpush()が最も単純で効率的な選択肢となります。データ構造やアプリケーションの要件に応じて、適切な追加方法を選択することが、効率的なプログラミングの鍵となります。

2次元配列のソート:意外と奥深い世界

1. 行単位でのソート(一次元配列としてのソート)

JavaScriptの2次元配列のソートは、単純な一次元配列のソートとは異なり、少し工夫が必要です。最も基本的なソートは、2次元配列の各行を独立した一次元配列として捉え、それらを特定の基準に基づいて並べ替える方法です。これは、各行が同じ型のデータセットを表し、そのデータセット全体を比較して順序を決定する場合に有効です。

JavaScriptのArray.prototype.sort()メソッドは、比較関数を引数に取ることで、カスタムなソートロジックを実装できます。この比較関数は、2つの要素(ここでは2次元配列の各行)を受け取り、それらの相対的な順序を示す数値を返します。


const data = [
  [3, 100, 'apple'],
  [1, 50, 'banana'],
  [2, 200, 'orange']
];

// 最初の要素(数値)を基準に行全体をソート
data.sort((a, b) => a[0] - b[0]);

console.log(data);
/*
出力:
[
  [1, 50, 'banana'],
  [2, 200, 'orange'],
  [3, 100, 'apple']
]
*/

この例では、各行の最初の要素(インデックス0)の数値に基づいて、行全体を昇順にソートしています。比較関数はa[0] - b[0]というシンプルな形式で、正の値ならbaより先に、負の値ならabより先に、ゼロなら順序変更なし、となります。

2. 特定の列をキーとしたソート

多くの場合、2次元配列のソートでは、特定の「列」の値をキーとして行全体を並べ替える必要があります。例えば、ユーザーリストを名前順にソートしたり、商品のリストを価格順にソートしたりする場合などです。この場合も、sort()メソッドとカスタム比較関数を使用しますが、比較関数内でアクセスするインデックス(列)を変更するだけです。

文字列をソートする場合は、数値とは異なる比較ロジックが必要です。localeCompare()メソッドは、文字列の比較に非常に便利で、国際化対応(ローカライズ)されたソート順にも対応できます。


const products = [
  ['Laptop', 1200, 'Electronics'],
  ['Mouse', 25, 'Electronics'],
  ['Keyboard', 75, 'Electronics']
];

// 商品名(インデックス0)を基準にソート
products.sort((a, b) => a[0].localeCompare(b[0]));

console.log(products);
/*
出力:
[
  ['Keyboard', 75, 'Electronics'],
  ['Laptop', 1200, 'Electronics'],
  ['Mouse', 25, 'Electronics']
]
*/

価格(インデックス1)でソートする場合は、再度数値比較に戻ります。このように、ソートの基準となる列を変更するだけで、様々なソート要件に対応できます。

3. 複数条件による複雑なソート

より高度なソートでは、複数の条件を組み合わせてソート順を決定する必要があります。例えば、まずカテゴリーでソートし、同じカテゴリー内では価格でソートするといった場合です。この場合、比較関数内で複数の条件を段階的に評価していきます。最初の条件で順序が決定されなかった場合(つまり、比較結果が0の場合)、次の条件で比較を続行します。


const items = [
  ['Pen', 100, 'Stationery'],
  ['Book', 500, 'Books'],
  ['Eraser', 50, 'Stationery'],
  ['Notebook', 300, 'Stationery'],
  ['Magazine', 450, 'Books']
];

// カテゴリー(インデックス2)でソートし、
// 同じカテゴリー内では価格(インデックス1)でソート
items.sort((a, b) => {
  // まずカテゴリーで比較
  const categoryCompare = a[2].localeCompare(b[2]);
  if (categoryCompare !== 0) {
    return categoryCompare;
  }
  // カテゴリーが同じ場合は価格で比較(昇順)
  return a[1] - b[1];
});

console.log(items);
/*
出力:
[
  ['Book', 500, 'Books'],
  ['Magazine', 450, 'Books'],
  ['Pen', 100, 'Stationery'],
  ['Eraser', 50, 'Stationery'],
  ['Notebook', 300, 'Stationery']
]
*/

このテクニックを使えば、非常に複雑なソートロジックも比較関数内にカプセル化することができます。ソートはデータの表示順序を決定するだけでなく、特定のデータを効率的に検索したり、分析したりするための前処理としても非常に重要です。データの特性と要件に合わせて、適切なソートロジックを設計しましょう。

3次元配列の基本と2次元配列との違い

1. 3次元配列の概念と構造

2次元配列が表形式のデータを表現するのに適しているのに対し、3次元配列はさらに一歩進んで、より複雑な階層構造を持つデータを表現するために使用されます。イメージとしては、2次元配列が「平面」であるなら、3次元配列は「立方体」や「複数の平面の集合」と考えると分かりやすいでしょう。これは、「配列の配列の配列」として構築され、それぞれが異なる次元のデータを保持します。

例えば、複数の都市の天気予報データを管理する場合を考えてみましょう。各都市(1次元目)には、複数日分のデータがあり(2次元目)、各日には最高気温、最低気温、天気などの情報(3次元目)が含まれます。


const weatherData = [
  // 都市1: 東京
  [
    // 1日目
    ['晴れ', 25, 18],
    // 2日目
    ['曇り', 23, 16],
    // 3日目
    ['雨', 20, 15]
  ],
  // 都市2: 大阪
  [
    // 1日目
    ['晴れ', 26, 19],
    // 2日目
    ['晴れ', 24, 17]
  ]
];

// 東京の1日目の天気予報
console.log(weatherData[0][0]); // 出力: ['晴れ', 25, 18]
// 東京の2日目の最高気温
console.log(weatherData[0][1][1]); // 出力: 23

このように、3次元配列はインデックスを3つ使用して特定のデータにアクセスします。ゲーム開発における3D空間の表現や、科学シミュレーションのデータなど、より多角的なデータ表現が求められる場面で活用されます。

2. 2次元配列との視覚的・論理的違い

2次元配列と3次元配列の最も大きな違いは、データを表現する「次元」の数です。

  • 2次元配列: 行と列の概念を持ち、スプレッドシートやテーブルのように、フラットな表面上にデータが配置されていると視覚化できます。インデックスは[行][列]のように2つ使用します。
  • 3次元配列: 行と列に加えて「深さ」や「層」の概念が加わります。複数の2次元配列が積み重なっているイメージや、各要素がさらに2次元配列であると考えることができます。インデックスは[層][行][列]のように3つ使用します。

論理的には、2次元配列は「属性の集合」を管理するのに適していますが、3次元配列は「属性の集合のさらに集合」を管理するのに適しています。例えば、学生の成績を例にとると、

  • 2次元配列: あるクラスの生徒ごとの科目点数 (例: [['Alice', 80, 90], ['Bob', 70, 85]])
  • 3次元配列: 複数のクラス(または学年)の生徒ごとの科目点数 (例: [[['Alice', 80, 90]], [['Charlie', 95, 88]]])

このように、管理したいデータの階層構造が深くなるにつれて、より高次元の配列が選択されることになります。次元が増えるほどデータへのアクセスは複雑になりますが、より現実に近いデータ構造をモデル化できるメリットがあります。

3. 多次元配列の次元を意識したデータ管理

多次元配列を扱う上で最も重要なのは、それぞれの次元が何を表しているのかを明確に意識することです。次元が増えるにつれて、コードの可読性が低下したり、エラーが発生しやすくなったりする傾向があるため、適切な命名規則やコメント、そして構造化されたアクセス方法が不可欠です。

例えば、先ほどの天気予報の例で、東京の3日間の最高気温の平均を計算する場合を考えてみましょう。


const weatherData = [
  // 都市1: 東京
  [
    ['晴れ', 25, 18], // 1日目 [天気, 最高, 最低]
    ['曇り', 23, 16], // 2日目
    ['雨', 20, 15]    // 3日目
  ]
  // ...他の都市
];

const tokyoHighestTemps = weatherData[0].map(dayData => dayData[1]); // 東京の各日の最高気温を抽出
const sumTemps = tokyoHighestTemps.reduce((acc, temp) => acc + temp, 0);
const averageTemp = sumTemps / tokyoHighestTemps.length;

console.log(`東京の3日間の最高気温平均: ${averageTemp}度`); // 出力: 東京の3日間の最高気温平均: 22.666...度

この例では、weatherData[0]が東京のデータにアクセスし、その中からmap()を使って各日の最高気温(インデックス1)を抽出しています。このように、それぞれの次元が何を意味するかを理解し、適切なメソッドやループを組み合わせてデータを操作することが、多次元配列を効率的かつ安全に管理する鍵となります。過度に次元を深くすると複雑性が増すため、データの構造とアプリケーションの要件を考慮し、最適な次元数を選択することが推奨されます。場合によっては、配列の配列よりも「オブジェクトの配列の配列」など、より意味的なデータ構造を検討するのも良いでしょう。

JavaScriptの便利な演算子とアロー関数を活用しよう

1. スプレッド構文(`…`)とレスト構文の活用

ES2015(ES6)以降に導入されたスプレッド構文(...)は、配列やオブジェクトを扱う際に非常に強力で便利な機能です。2次元配列の操作においても、その柔軟性が大いに役立ちます。スプレッド構文を使用することで、配列の要素を簡単に展開したり、新しい配列を既存の配列から作成したり、あるいは関数に引数を渡したりすることができます。

特に2次元配列では、「非破壊的な更新」を行う際に重宝します。これは、元の配列を変更せずに、新しい配列を生成して変更を加えるというプログラミングパターンで、特にReactのようなフレームワークでは状態管理の基本となります。


const originalMatrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

// 行を追加して新しい2次元配列を作成
const newMatrixWithRow = [...originalMatrix, [10, 11, 12]];

console.log('行追加後:', newMatrixWithRow);
// 出力: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
console.log('元の行列:', originalMatrix);
// 出力: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] (変更なし)

// 既存の行を更新した新しい2次元配列を作成
const updatedMatrix = originalMatrix.map((row, index) =>
  index === 1 ? [100, 200, 300] : row // 2行目を更新
);

console.log('行更新後:', updatedMatrix);
// 出力: [[1, 2, 3], [100, 200, 300], [7, 8, 9]]

レスト構文も同様に...を使用しますが、こちらは関数引数や配列の分割代入において、残りの要素を一つにまとめる役割を果たします。これらを組み合わせることで、より簡潔で読みやすいコードを書くことができます。

2. アロー関数を用いた簡潔な処理

アロー関数(=>)もES6で導入され、特にコールバック関数として使用する場合にコードを大幅に簡潔にする効果があります。map()filter()reduce()sort()といった配列の高階関数と組み合わせることで、2次元配列の要素に対する変換、フィルタリング、集計などの操作を非常に読みやすく記述できます。

従来のfunctionキーワードを使った関数宣言と比較して、アロー関数はいくつかの特徴があります。

  • functionキーワードが不要。
  • 引数が1つの場合は括弧()を省略可能。
  • 処理が1行の場合は{}returnを省略可能。
  • thisの挙動が従来の関数とは異なり、定義されたスコープのthisを継承する(レキシカルthis)。

const studentData = [
  ['Alice', 85],
  ['Bob', 92],
  ['Charlie', 78]
];

// フィルタリング: 点数が90点以上の生徒を抽出
const highAchievers = studentData.filter(student => student[1] >= 90);
console.log('高得点者:', highAchievers);
// 出力: [['Bob', 92]]

// マッピング: 生徒の名前だけを抽出
const studentNames = studentData.map(student => student[0]);
console.log('生徒の名前:', studentNames);
// 出力: ['Alice', 'Bob', 'Charlie']

このように、アロー関数は2次元配列の各要素に対する反復処理を、非常にすっきりと表現するのに役立ちます。短い記述で意図が明確になるため、コードの可読性と保守性が向上します。

3. 分割代入によるデータ抽出

分割代入(Destructuring assignment)は、配列やオブジェクトから値を抽出し、個別の変数に割り当てるための便利な構文です。2次元配列を扱う際にも、特定の行やその中の要素を効率的に抽出するのに役立ちます。これにより、コードの冗長性を減らし、より簡潔で理解しやすい変数割り当てが可能になります。

特に、配列の要素が複数の意味を持つ場合、分割代入を使ってそれぞれの意味に応じた変数名を割り当てることで、コードの可読性が格段に向上します。


const employee = ['John Doe', 'Software Engineer', 60000];

// 配列の要素を個別の変数に分割代入
const [name, position, salary] = employee;

console.log(`名前: ${name}`);       // 出力: 名前: John Doe
console.log(`役職: ${position}`);    // 出力: 役職: Software Engineer
console.log(`給与: $${salary}`);    // 出力: 給与: $60000

// 2次元配列の特定の行を抽出し、さらにその要素を分割代入
const teams = [
  ['Alpha', 'Alice', 'Bob'],
  ['Beta', 'Charlie', 'David']
];

const [, teamBeta] = teams; // 2番目のチームを抽出
const [, leaderBeta, memberBeta] = teamBeta; // teamBetaの要素を分割代入

console.log(`Betaチームのリーダー: ${leaderBeta}`); // 出力: Betaチームのリーダー: Charlie

分割代入は、関数から複数の値を返す場合や、配列の特定のインデックスの要素にアクセスする際に特に強力です。スプレッド構文やアロー関数と組み合わせることで、JavaScriptでのデータ処理をより効率的かつエレガントに行うことができます。これらのモダンなJavaScriptの機能は、2次元配列だけでなく、あらゆるデータ構造の操作において、開発の生産性を大きく向上させるでしょう。