JavaScript

JSのObject参照について

やりたかったこと

一つの連想配列(オブジェクト)を利用して、複数のオブジェクトを持った配列を作成。
特定のプロパティの値に対して連番を振りたかった。

コード

const makeObjArr = (obj, num) => {
    const results = [];
    for (let i = 1; i <= num; i++) {
        results.push(obj);
    }
    return results;
};

const incrementObj = (arr, targetKey) => {
    return arr.map((item, idx) => {
        item[targetKey] = item[targetKey] + String(idx + 1);
        return item;
    });
};

const a = {hoge: "hoge"};
const b = makeObjArr(a, 3);
console.log(incrementObj(b, "hoge"));

理想

{hoge: "hoge"} => [{hoge: "hoge1"}, {hoge: "hoge2"}, {hoge: "hoge3"}]

現実

{hoge: "hoge"} => [{hoge: "hoge123"}, {hoge: "hoge123"}, {hoge: "hoge123"}]

Q.なぜこうなった?

A. JSのObjectは参照渡しだから。
JSではプリミティブデータ型(Boolean, Null, Undefined, Number, String, Symbol)は値渡し。
Objectは参照渡しとなっています。

例を挙げてみると

String

/* String */
var a = "a";
var b = a;
b = "b";
console.log(a);
console.log(b);
console.log(a === b);
a
b
false

Number

/* Number */
var a = 1;
var b = a;
b = 2;
console.log(a);
console.log(b);
console.log(a === b);
1
2
false

Symbol

/* Symbol */
var a = Symbol("a");
var b = a;
b = Symbol("b");
console.log(a);
console.log(b);
console.log(a === b);
Symbol(a)
Symbol(b)
false

コードを書いていて想定通りの動きになっているはず。
変数aの値は書き換えられず、変数bの値だけが書き換わっている状態。

ところがObjectだと・・・

/* Object */
var a = {value: "a"};
var b = a;
b["value"] = "b";
console.log(a);
console.log(b);
console.log(a === b);
{ value: 'b' }
{ value: 'b' }
true

見事にaのvalueプロパティの値が書き換えられている・・・。
プリミティブ型だと変数宣言時にメモリ割当が行われるが、Objectだと初期化時、生成時にメモリ割当が行われるということでしょう。

/* {}とnew ObjectはObjectの生成/初期化を意味する */
var a = {};
var b = new Object;
var c = a;
console.log(a);
console.log(b);
console.log(a === b);
console.log(a === c);
c = {};
console.log(a === c);
{}
{}
false
true
false

で、どうすれば理想が現実になるの?

Object.assign()メソッドを使用する。

const makeObjArr = (obj, num) => {
    const results = [];
    for (let i = 1; i <= num; i++) {
        results.push(Object.assign({}, obj));
    }
    return results;
};

const incrementObj = (arr, targetKey) => {
    return arr.map((item, idx) => {
        item[targetKey] = item[targetKey] + String(idx + 1);
        return item;
    });
};

const a = {hoge: "hoge"};
const b = makeObjArr(a, 3);
console.log(incrementObj(b, "hoge"));
[ { hoge: 'hoge1' }, { hoge: 'hoge2' }, { hoge: 'hoge3' } ]

Object.assign()
一つ以上の ソース オブジェクトから、直接所有で (own) 列挙可能な (enumerable) すべてのプロパティの値を、ターゲット オブジェクトへコピーします。戻り値はターゲット オブジェクトです。

素敵。
ただし、IEでは動作しない可能性があるので、そこは注意が必要。
Babelとか使ってなくて、IE対応があるから使えない・・・って場合だったらfor文で頑張って下さい。