APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

JavaScriptで配列の初期値に空の配列をセットする際に気をつけるべき点

先進サービス開発事業部の高橋です。主にフロントエンドの開発を担当しています。今回はちょっとした小ネタを。 タイトルは意味が伝わりにくいですが、要は以下のような2次元配列をあらかじめ作りたかったということです。

[[], [], [], ...] // 配列の中に空の配列が値として入っている状態。 [] は配列

それでどのようにしたかというと、以下のような形で配列を初期化して値をセットしました。

let hairetsu = Array(N).fill([]);

Array.prototype.fill(value, start, end)は配列を開始位置から終了位置まで指定した値で埋めるメソッドになります。 第二、第三引数のstart,endを指定しなければ最初から最後までうまる形になります。

私の目論見では、上記のように配列にそれぞれ空の配列が N 個セットされているはずでした。 そしてそれ自体は目論見通りになっていたのですが、そこから先の挙動がどうにもおかしい。

どうおかしいかというと、以下のような現象が発生しました。

// 配列の長さが3で、それぞれに空の配列をセットする。
let hairetsu = Array(3).fill([]);

// 各々の配列に値をそれぞれ追加する。
hairetsu[0].push('ゴリラ');
hairetsu[1].push('ラッパ');
hairetsu[2].push('パセリ');

console.log(hairetsu[0]);

// 期待する値: Array['ゴリラ']
// 実際の値: Array['ゴリラ', 'ラッパ', 'パセリ']

!???

0番の配列に追加したのはゴリラだけなのに対して、ラッパもパセリも追加されていました。

原因

まず、前提の話をすると、JavaScriptが取り扱うデータはプリミティブ型とオブジェクト型の2つに大別されます。 プリミティブ型は以下の6つです。

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Symbol

対してオブジェクト型は以下の通りです。

  • Object
  • Array

JavaScriptではオブジェクト型を関数・メソッドの引数に渡すと問答無用で参照渡しになるという仕様があります。 つまり配列を初期化した時の Array.prototype.fill()メソッドに渡した空の配列は表向き個別に作られたように見えますが、実は同一のポインタを参照しているコピーとということになります。

もちろん、ある程度経験のあるJavaScripterなら一度は遭遇したことのある事象なのですが、標準ビルトインオブジェクトのメソッドだし、なんとなく意識の外にありました。。

というかMDNを確認するとちゃんと書いてありました。

fill にオブジェクトを渡した場合、そのオブジェクトへの参照がコピーされ、配列に参照が書き込まれます。

MDN Web Docs | Array.prototype.fill()

では、どうするか。を書いていきます。

愚直にやる

愚直にやるならなんてことはありません。初期化した後にループして空の配列を代入するだけです。

let hairetsu = Array(3);

for (let i = 0, l = hairetsu.length; i < l; i++) {
    hairetsu[i] = [];
}

// 各々の配列に値をそれぞれ追加する。
hairetsu[0].push('ゴリラ');
hairetsu[1].push('ラッパ');
hairetsu[2].push('パセリ');

console.log(hairetsu[0]);

// 期待する値: Array['ゴリラ']
// 実際の値: Array['ゴリラ']

素直になるならこうなるかと思います。あんまりスマートじゃないので良い書き方があれば良いのですが。