photo credit: diana_robinson The Milky Way over a radio telescope at the Karl G. Jansky Very Large Array National Radio Astronomy Observatory in New Mexico via photopin (license)
JavaScriptで、あらかじめ一定の長さの配列に初期値を放り込んでおくために Array.fill
を使いますが、勝手な思い込みからハマってしまいました。恥ずかしながら、その詳細をご報告。
杉本 雅広(著)
新品 ¥2,905 8個の評価
Amazon.co.jpで詳細を見る
Array.fill はどんな関数?
Array.fill は、配列の全要素に同じ値を設定する関数です。 以下の例では、3個の数値配列を生成して値 0 で初期化しています。
const arr = Array(3).fill(0);
余談ですが、fillで初期化しておかないと、要素はすべて undefined
になっており、そのままでは forEach などの列挙メソッドが回りません。
Objectで初期化したらおかしな挙動
以下のように初期値にObjectを与えていたのですが、思った通りの動きをしてくれませんでした。
const arr = Array(3).fill( { x: 0, y: 0 } );
3個の二次元座標値を持つ配列。。。のつもりですが、以下のようなことになりました。
const arr = Array(3).fill( { x: 0, y: 0 } ); arr[0].x = 10; arr[0].y = 8; console.log(JSON.stringify(arr)); //出力:[{"x":10,"y":8},{"x":10,"y":8},{"x":10,"y":8}]
0番目の情報を書き換えただけで、他のすべてのデータが書き換わってしまいました。 配列要素は確かに3つあるのですが、全ての要素が、ひとつの座標値を参照しているのです。
なぜこうなるか
JavaScriptのObjectはメソッドの引数や代入時には、必ず参照が渡されますから、このようなことが起こります。 ソースコードをよく見れば、fill に与えられている座標値は、1度しか生成されていません。 だから直感的でないにせよ、これは当たり前の挙動です。 つまり、ワタシが勝手に「別の複数のオブジェクトが生成される」と思い込んでいただけなのでした。
解決方法
唯一の正しい答えなんてありませんから、3つほどの解決方法を示してみます。 プリミティブな型なら最初の方法が良いと思いますが、プロパティを持つオブジェクトならば2番めの方法が好みです。 関数プログラミング的には最初の方法が正解だと思われますが。
(1) まるごと代入すれば良い
上の例の2行目と3行目を以下のように変えれば、別のオブジェクトを生成して代入していることになるので、なんら問題ありません。
const arr = Array(3).fill( { x: 0, y: 0 } ); arr[0] = { x: 10, y: 8 }; console.log(JSON.stringify(arr)); //出力:[{"x":10,"y":8},{"x":0,"y":0},{"x":0,"y":0}]
ただ、これがベストだとは思いません。 なんて言いましょうか、初期化のコードとまるごと代入部分で、統一感がないというか、思想に差があるというか、そんな妙なミスマッチ感がありますね。 最初は全要素がx,yともに0の単一オブジェクトを参照していますが、その後、値としては同じ(x,yとも0である)データを代入したなら、別のインスタンスを参照することになる。
あと、誰かがうっかり別のところで要素の「プロパティを書き換えちゃった」というような不測の(しかし予測可能な)事態に対して脆弱です。構文的な防御策がありませんからね。
加えて、多くのプロパティを持つオブジェクトである場合、インスタンスの生成コストも気になります。本来、気にするべきではないのですが。
(2) 別オブジェクトで初期化する
初期化時点で別のオブジェクトを設定しておくには、以下のように書けばよろしいかと思います。
const arr = Array(3).fill().map(e=>({ x: 0, y: 0 }));
fill()
は fill(null)
と同じです。全要素をnull
で初期化しています(初期値は nullでなくても undefined 以外ならOKです)。
これをしておかないと全ての要素が undefined
になっていて、うしろの map
が回ってくれないのです。
こうしておけば、以下のように間違いは発生しません。
const arr = Array(3).fill().map(e=>({ x: 0, y: 0 })); arr[0].x = 10; arr[0].y = 8; console.log(JSON.stringify(arr)); //出力:[{"x":10,"y":8},{"x":0,"y":0},{"x":0,"y":0}]
Ethan Brown(著), 武舎 広幸(翻訳), 武舎 るみ(翻訳)
新品 ¥3,520 17個の評価
Amazon.co.jpで詳細を見る
(3) ループを回して初期化する
また、以下のようにループを回して初期化する方法もありますが、前時代的ではありますね。
const arr = Array(3); for(let i = 0; i < arr.length; i++) { arr[i] = { x: 0, y: 0 }; }