つまらないことでハマったので備忘録を残します。
やりたいこと
長さの決まった2次元配列(arr
)にarr[index]
でアクセスして値を更新したい。
問題が起きるコード
(function(size){
const arr = new Array(size).fill([]);
// arr[index]にアクセスする何らかの処理
for(let i=0;i<arr.length;i++){
arr[i].push(i);
}
for(let i=0;i<arr.length;i++){
console.log(arr[i]);
}
})(10)
想像していた出力は以下の通りです。
// debugger eval code
Array [ 0 ]
Array [ 1 ]
Array [ 2 ]
Array [ 3 ]
Array [ 4 ]
Array [ 5 ]
Array [ 6 ]
Array [ 7 ]
Array [ 8 ]
Array [ 9 ]
しかし、得られた出力はこちらです。
// debugger eval code
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
原因
完全に私の理解度の問題でArray.prototype.fill
の挙動を勘違いしていました。
よく考えれば当たり前ですが、fill
メソッドでは再帰的に新しいインスタンスを作成するのではなく、単純にオブジェクトの代入を行います。
上記のコードでは初期化部分に問題があり、初期化部分は以下のコードと等価になります。
// 問題の起きるコード
const arr = new Array(size).fill([]);
// 問題の起きるコードと等価のコード
// size = 10
const ref = [];
const arr = [ ref, ref, ref, ref, ref, ref, ref, ref, ref, ref ];
基本仕様として、JavaScriptは値(number | string | boolean
など)は値の代入、オブジェクトは参照を代入します。
上記のコードで何が起きるかというと、arr[index]
で参照される値はすべてref
となってしまうためarr[index].push
した場合は要素全てに再帰的にpush
したような挙動になります。
解決策
インスタンスを新しく作成すれば問題を回避できるのでArray.prototype.map
やArray.prototype.filter
を使用すれば回避できます。(もっと簡潔な解決策があれば教えて下さい...)
// 抜粋
const arr = new Array(size).fill(0).map(n => []);
(function(size){
const arr = new Array(size).fill(0).map(n => []);
// arr[index]にアクセスする何らかの処理
for(let i=0;i<arr.length;i++){
arr[i].push(i);
}
for(let i=0;i<arr.length;i++){
console.log(arr[i]);
}
})(10)
参考
[1] MDN, Array
, https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array
[2] MDN, Array.prototype.fill
, https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/fill