JavaScriptにおけるプリミティブ型とオブジェクト型の挙動の差

2021年1月21日JavaScript

プリミティブ型(primitives)とオブジェクト型(objects)についてまとめました。
プリミティブ型とオブジェクト型で値を変更したとき、どのような結果になって、なぜそれが異なる結果になるのか、理解しておかないとバグの原因になりますので、詳しくその背景を知っておきましょう。

プリミティブ型とオブジェクト型の挙動の具体例

まずは、どのような違いがあるのか、プリミティブ型とオブジェクト型の挙動の具体例をみておきましょう。

// プリミティブ型(primitives)
let age = 40;
let oldAge = age;
age = 41;
console.log(age); // 41
console.log(oldAge); //40

//オブジェクト型(objects)
const me = {
  name: 'yamada';
  age: 40,
};

const friend = me;
frined.age = 50;
console.log('Friend:', friend);// Friend {name: 'yamada', age:50}
console.log('Me:', me');// Friend {name: 'yamada', age:50}

まずプリミティブ型は上の例のようにageに41、oldAgeに40と異なる値が取得できます。
しかし、オブジェクトでは、同じ参照先がコピーされているので、meもfriendも同じくageが変更されています。

プリミティブ型とオブジェクト型について

プリミティブ型

プリミティブ型(primitives)とは次のことです。

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

プリミティブ型のメモリータイプ

プリミティブ型のメモリータイプはprimitive typesです。

オブジェクト型

オブジェクト型(objects)はプリミティブ型以外のものを指します。
つまり、次のようなものです。

  • Object literal
  • Arrays
  • Functions
  • その他

オブジェクト型のメモリータイプ

オブジェクト型のメモリータイプはreference typesです。

メモリータイプ:primitive typesとreference typesの違い

JavaScriptエンジンの中身はコールバックとヘルプがありました。
ヘルプはオブジェクトを保管しているメモリーであり、referenceタイプのオブジェクトはヘルプに保管されます。
その一方で、primitiveタイプは、コールスタックに保管されます。

コールスタックのidentifier

プリミティブ型の保管と変更の挙動

プリミティブ型が保管されるコールスタックの中の挙動をみてみましょう。
コールスタックの中にはidentifierがあり、ageという変数は、address:00001で、そのvalueは40というように、identifierはaddressとvalueをまとめています。

oldAge = ageという意味は、そのアドレスをそのまま参照します。つまり、oldAgeはaddress:00001で、そのvalueは40という情報を参照します。

さらに、新しくageが変更されると、address:00002で、そのvalueは41という情報が新しくつくられます。

そのためageとoldAgeでは違う値が出力されます。

オブジェクト型の保管と変更の挙動

オブジェクト型も同じくaddress:00003と割り当てられるのですが、値の部分で挙動が変わります。
プリミティブ型とは違い、そのvalueそのものをそのアドレスに割り当てるのではなく、ヘルプに保管されているアドレスを参照先としてvalueに保管します。

そして、friendがmeをコピーすると、それはつまりaddress:00003とヘルプの参照先をコピーすることになります。
meもfriendも同じところに参照先がある状態ということです。

そのためヘルプの中にある値が変更されたとしても、参照先が変更されるわけではないため、どちらも同じように変化してしまいます。

プリミティブ型とオブジェクト型の挙動の具体例2

 // プリミティブ型(primitives)
let lastName = 'yamada';
 let oldLastName = lastName;
 lastName = 'Sasaki';
 console.log(lastName, oldLastName);

 //オブジェクト型(objects)
 const hanako = {
   firstName = 'hanako',
   lastName = 'yamada',
   age: 30,
 };
 const marriedHanako = hanako;//ヘルプの参照先をコピーした
 marriedHanako.lastName = 'sasaki';
 console.log('結婚前', hanako); //lastNameはsasakiになる
 console.log('結婚後', marriedHanako); //lastNameはsasakiになる

このように、オブジェクトhanakoもオブジェクトmarriedHanakoもただ名前が違うだけで中身は全く同じものということになります。
たとえconstで宣言しても、コールスタック内に保持されているアドレスと参照先は変化しないため、ヘルプ内の値を変更することはできます。

では本当にオブジェクトをコピーしたいときはどうしたらいいのでしょうか?
その方法をみてみてましょう。

オブジェクトをコピーしたいときの解決方法

本当にオブジェクトをコピーしたいときは、object.assign()を使います。
具体的にみていきましょう。

 //オブジェクト型をコピーする
const yamada = {
  firstName = 'hanako',
  lastName = 'yamada',
  age: 30,
  family: ['たける', 'きょうこ'],
};

const yamadaCopy = object.assign({}, yamada2);
yamadaCopy.lastName = 'sasaki';
console.log('結婚前', yamada); //lastNameはyamadaのまま
console.log('結婚後', yamadaCopy); //lastNameはsasakiになる

上のように、object.assign()を使うことでlastNameは結婚前はyamadaのままで結婚後にsasakiに変化されることができます。
ただし、これは、first-levelのみコピーしている点に注意しまよう。
どういうことは、下の例をみてみましょう。

object.assign()はshallow copyという具体例

//オブジェクト型をコピーする
const yamada = {
 firstName = 'hanako',
 lastName = 'yamada',
 age: 30,
 family: ['たける', 'きょうこ'],
};

const yamadaCopy = object.assign({}, yamada2);
yamadaCopy.lastName = 'sasaki';
console.log('結婚前', yamada); //lastNameはyamadaのまま
console.log('結婚後', yamadaCopy); //lastNameはsasakiになる

//shallow copyという例
yamadaCopy.family.push('しんじ');
yamadaCopy.family.push('たろう');

console.log('結婚前', yamada); //familyが4人になる
console.log('結婚後', yamadaCopy); //familyが4人になる

そのため、shallow-copyといわれます。deep cloneを作っているわけではなく、それ以上の深い階層の情報はコピーシていない点に注意が必要です。

うえのshallo copyという部分をみてもらうとわかる通り、結婚前は家族が2人で結婚後は4人になるはずが、結婚前も結婚後も家族が4人になってしまいます。
それは、オブジェクトの中の配列の中のことはコピーできていないからです。