JavaScript の配列やオブジェクトは参照渡しになる…バグを生む落とし穴

JavaScript において、配列やオブジェクトは参照渡しになる。コレが思わぬバグを生むことに繋がるので、紹介しておく。

今回、「参照渡し」「値渡し」「参照の値渡し」などの議論は避けるが、オブジェクトを代入する際、以下のような挙動をすることは覚えておきたい。

// 初期値としてこの値をそのまま保持しておきたい
const defaultValue = {
  title: 'ほげほげ',
  itemsCount: 0,
  pageNumber: 1
};

// myValue は処理中で変更が入る要素 … 最初に初期値を代入する
let myValue = defaultValue;

// 処理中に値を変更する
myValue.title = 'ふがふが';
myValue.itemsCount = 100;

// 当然 myValue に変更は反映されているが…
console.log(myValue.title);      // → 'ふがふが'
console.log(myValue.itemsCount); // → 100

// 実は defaultValue まで書き換わっている
console.log(defaultValue.title);      // → 'ふがふが' ('ほげほげ' ではない)
console.log(defaultValue.itemsCount); // → 100        (0 ではない)

// だから「また初期値に戻そう」と思って defaultValue を代入しても、元に戻せない
myValue = defaultValue;
console.log(myValue.title);      // → 'ふがふが' ('ほげほげ' に戻らない!)
console.log(myValue.itemsCount); // → 100        (0 に戻らない!)

コレは Object (連想配列) だけでなく、Array (配列) も同様。

対策としては、連想配列や配列そのものを代入して渡すのではなく、その配列のコピーを生成して渡すのが正解になる。

一番簡単な例では、Object.assign() を使う例。

// 連想配列のコピーには Object.assign() が使える
// ただし、オブジェクト内にオブジェクトがある場合は、中のオブジェクトが参照渡しになってしまうので不完全
let myValue = Object.assign({}, defaultValue);

オブジェクト内のオブジェクトが参照渡しになってしまう件は、JSON.stringify()JSON.parse() を組み合わせたやり方だと解消できる。オブジェクトを一度文字列化してから再度オブジェクトに戻すことで複製するのだ。

ただし、このやり方は Date 型のプロパティ値を正しく復元できなくなるという問題がある。

let myValue = JSON.parse(JSON.stringify(defaultValue));

一番良いのは、Lodash の cloneDeep() を使うと確実にコピーできる。

// Lodash の cloneDeep() が一番確実
let myValue = _.cloneDeep(defaultValue);

_.cloneDeep()_.clone(isDeep) のエイリアス。_.clone(defaultValue, true) と書けば _.cloneDeep(defaultValue) 同様にディープコピーになるが、_.clone(defaultValue) と第2引数を書き忘れると参照渡しになってしまうので、書き忘れを防ぐために _.cloneDeep() を使うと良いだろう。

なお、配列の場合は concat() などでコピーできる。

const defaultArray = [ 1, 2, 3 ];
let myArray = defaultArray.concat();