JavaScriptにおいて、グリッドデータや行列計算、ゲームのマップ情報などを扱う際に「2次元配列」は欠かせないデータ構造です。
しかし、JavaScriptの配列は他のプログラミング言語とは異なる独自の挙動を持っており、特に初期化の段階で「意図しない挙動」に悩まされる開発者が少なくありません。
初心者から中級者までが陥りやすい「参照渡しの罠」を回避し、安全かつ効率的に2次元配列を宣言する方法を詳しく見ていきましょう。
この記事では、モダンなJavaScriptにおける最適な初期化パターンと、それぞれの記述法がメモリ上でどのように振る舞うのかを論理的に解説します。
JavaScriptにおける2次元配列の概念
JavaScriptには、厳密な意味での「多次元配列」という専用のデータ型は存在しません。
私たちが一般的に2次元配列と呼んでいるものは、実際には「配列の要素としてさらに配列が格納されている構造」を指します。
つまり、「配列の配列(Array of Arrays)」として実装されているのがJavaScriptの特徴です。
この構造を理解していないと、動的な配列生成時に予期せぬバグを引き起こす原因となります。
基本的な2次元配列の構造は、以下のようなリテラル記法で表現されます。
// 2行3列の2次元配列をリテラルで定義
const matrix = [
[1, 2, 3],
[4, 5, 6]
];
console.log(matrix[0][1]); // 1行目の2番目の要素にアクセス
2
このように、インデックスを2つ繋げて記述することで、特定の要素にアクセスすることが可能です。
静的なデータであればこの記法で十分ですが、プログラムの実行時にサイズが決まる動的な配列を扱う場合には、より高度な初期化手法が必要になります。
【注意】Array.fill()による初期化で発生する問題
多くの開発者が最初に行い、そして失敗してしまうのが、Array.prototype.fill()メソッドを使用した初期化です。
例えば「3行3列の、すべての要素が0の配列」を作ろうとして、次のようなコードを書いたとしましょう。
// 注意:この書き方は推奨されません
const rows = 3;
const cols = 3;
const badMatrix = new Array(rows).fill(new Array(cols).fill(0));
// 1か所だけ値を変更してみる
badMatrix[0][0] = 9;
console.log(badMatrix);
[
[9, 0, 0],
[9, 0, 0],
[9, 0, 0]
]
実行結果を見ると、「0行目の0列目だけを9に変えたはずなのに、すべての行の0列目が9になってしまった」という怪奇現象が発生しています。
なぜ意図しない挙動が発生するのか
この現象の原因は、JavaScriptにおける「参照(リファレンス)」の扱いにあります。
fill()メソッドは、引数に渡された値をすべての要素にコピーしますが、引数がオブジェクト(配列を含む)の場合、その「メモリ上の参照先」をコピーしてしまいます。
つまり、badMatrixの各要素(各行)は、すべて全く同じ一つの配列インスタンスを指し示している状態になります。
どこか一行を書き換えるということは、すべての行の実体となっている単一の配列を書き換えることに他なりません。
このようなバグは一見して気づきにくいため、大規模なアプリケーションでは致命的なエラーに繋がる恐れがあります。
正しく2次元配列を初期化する推奨コード
では、各行が独立したインスタンスとして生成される正しい初期化方法はどのようなものでしょうか。
モダンなJavaScript(ES6以降)において、最も推奨されるのはArray.from()を活用する方法です。
Array.from()を活用した初期化
Array.from()は、第2引数にコールバック関数(マップ関数)を取ることができます。
このコールバック関数は、配列の要素が生成されるたびに新しく実行されるため、各行に独立した配列を割り当てることが可能です。
const rows = 3;
const cols = 3;
// 正しい初期化方法:各行で新しい配列を生成する
const correctMatrix = Array.from({ length: rows }, () => new Array(cols).fill(0));
// 1か所だけ値を変更
correctMatrix[0][0] = 9;
console.log(correctMatrix);
[
[9, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
この方法であれば、0行目だけが正しく書き換わっていることが確認できます。
() => new Array(cols).fill(0)の部分が、行数分だけ個別に実行されることがポイントです。
map()メソッドを併用する場合の注意点
既存の配列からmap()を使って2次元配列を作ることも可能ですが、ここでも注意点があります。
new Array(rows).map(...)と記述しても、空の配列に対してはmapのコールバックが実行されません。
空スロットを埋めるために一度fill()を噛ませるか、スプレッド構文を利用する必要があります。
// fillとmapを組み合わせる手法
const matrixWithMap = new Array(3).fill(null).map(() => new Array(3).fill(0));
この記法も一般的ですが、Array.from()の方が記述が簡潔であり、意図が明確に伝わりやすい傾向にあります。
for文を用いた柔軟な2次元配列の作成
関数型の書き方だけでなく、伝統的なfor文を用いた初期化も依然として有効です。
特に、初期値が単純な定数ではなく、行列のインデックスに基づいた計算が必要な場合などは、ループ文の方が可読性が高まることがあります。
const rows = 3;
const cols = 3;
const matrix = [];
for (let i = 0; i < rows; i++) {
const row = [];
for (let j = 0; j < cols; j++) {
// 行と列のインデックスを足した値を初期値にする例
row.push(i + j);
}
matrix.push(row);
}
console.log(matrix);
[
[0, 1, 2],
[1, 2, 3],
[2, 3, 4]
]
for文による初期化のメリットは、記述が直感的であり、実行速度が非常に高速である点にあります。
複雑な初期化ロジックが必要な際や、数百万要素を超えるような極めて巨大な配列を扱うパフォーマンス重視の場面では、Array.from()よりもfor文が適しています。
多次元配列を扱う際のパフォーマンス面での考慮事項
2次元配列を扱う上で避けて通れないのが、パフォーマンスとメモリ効率の問題です。
JavaScriptの配列は「ハッシュマップ」に近い実装となっている場合があり、連続したメモリ領域を確保するC言語などの配列とは性質が異なります。
TypedArray(型付き配列)の検討
もし扱うデータが数値のみであり、かつパフォーマンスが極めて重要(画像処理や音声解析など)な場合は、2次元配列をあえて使わずに、「1次元の型付き配列」を2次元として扱う手法が取られます。
// 3x3の行列を1次元配列として確保
const size = 3;
const typedMatrix = new Int32Array(size * size);
// (row, col) = (1, 2) へのアクセス
// インデックスの計算式:row * size + col
typedMatrix[1 * size + 2] = 42;
console.log(typedMatrix);
この手法は「フラット化(Flattening)」と呼ばれ、メモリの断片化を防ぎ、CPUのキャッシュ効率を劇的に向上させます。
通常のアプリケーション開発ではここまでする必要はありませんが、知識として持っておくと最適化の幅が広がります。
各初期化手法の比較まとめ
これまで紹介した主な初期化手法の特徴を、以下の表にまとめました。
| 手法 | 安全性 | コードの簡潔さ | パフォーマンス | 用途 |
|---|---|---|---|---|
リテラル記法 [[]] | 最高 | 最高 | 最高 | 少量の固定データ定義 |
Array.from() | 高い | 高い | 標準的 | 動的な配列生成の標準 |
fill().map() | 高い | 標準的 | 標準的 | 関数型プログラミングを好む場合 |
for文によるループ | 高い | 低い | 高い | 複雑な初期化や高性能を求める場合 |
new Array().fill() | 極めて低い | 高い | 高い | 非推奨(参照バグの元) |
基本的には、「Array.from()」を第一選択肢とし、パフォーマンスがボトルネックになる場合のみ「for文」や「1次元配列へのフラット化」を検討するのが、現代的なJavaScript開発における正攻法と言えるでしょう。
まとめ
JavaScriptでの2次元配列の宣言・初期化は、一見シンプルに見えて「参照の共有」という落とし穴が潜んでいます。
new Array().fill([])のような書き方は、すべての行が同一の配列を参照してしまうため、必ず避けるようにしてください。
安全に2次元配列を作成するためには、Array.from()を使用して各行を独立したインスタンスとして生成するか、古典的なfor文による明示的な初期化を行いましょう。
データ構造を正しく初期化することは、バグの混入を防ぐだけでなく、その後のデータ操作をより直感的で確実なものにします。
本記事で紹介した手法を活用し、意図しない挙動に悩まされない、堅牢なJavaScriptプログラムを作成してください。
