Core JavaScript 1.データタイプ - 基本型データと参照型データ

この記事はweb開発に欠かせないJavaScriptのコアな概念をしっかり理解しようということでCore Javascript(韓国語)を要約した内容になります。

基本型データ参照型データ

不変値

変数と定数を区分する性質は「変更可能性」で、その対象となるのはめもりの「変数領域」である。また、不変性を区分する対象は「データ領域」だ。

基本型の数字、文字列、null、undefined、Symbolはすべて不変値である。

var a = 'abc';
a = a + 'def';

var b = 5;
var c = 5;
b = 7;

このように変更は必ず新しい値を作って代入する振る舞いをする。これがすなわち「不変性」である。 (一度作られた値はガベージコレクタによって開放されないうちには絶対に変わらない)

可変値

基本型が不変性を持っていることに対して参照型データは基本的に可変値である。 ただし、場合によっては変更不可なこともあるし、不変値として扱うこともある。

var obj1 = {
  a: 1,
  b: 'bbb'
};

変数領域

アドレス 1001 1002 1003 1004
データ     名前: obj1
値: @5001
     

データ領域

アドレス 5001 5002 5003 5004
データ   @7103 ~ ?   1 ‘bbb’  

@5001の変数領域

アドレス 7103 7104 7105 7106
データ   名前: a
値: @5003
名前: b
値: @5004
     

オブジェクトの変数(プロパーティ)領域が別途存在するため、プロパーティはいくらでも変えることができる。 ということで参照型データは可変値(Immutable)だと言える。

プロパーティの再割り当て

var obj1 = {
  a: 1,
  b: 'bbb'
};
obj1.a = 2;

5行目で a2を割り当てようとしている。 データ領域に2が存在しないため、任意の空間に2を保存しそのアドレスを@7103に保存する。 この振る舞いによってobj1がアドレスは@5001のままで変わらない。 新しいオブジェクトが作られたわけではなく、オブジェクト内部の値だけが変わった結果になる。

ネストされた参照型データのプロパーティ割り当て

var obj = {
  x: 3,
  arr: [3, 4, 5]
}

変数領域

アドレス 1001 1002 1003 1004 1005
データ   名前: obj
値: @5001
       

データ領域

アドレス 5001 5002 5003 5004 5005
データ @7103 ~ ? 3 @8104 ~ ? 4 5  

オブジェクト@5001の変数領域

アドレス 7103 7104
データ 名前: x
値: @5002
名前: arr
値: @5003
 

オブジェクト@5003の変数領域

アドレス 8104 8105 8106
データ 名前: 0
値: @5002
名前: 1
値: @5004
名前: 2
値: @5005
 

obj.arr[1]を検索しようとすると以下のような手順でデータを取得することになる

@1002 -> @5001 -> (@7103 ~ ?) -> @7104 -> @5003 -> (@8104 ~ ?) -> @8105 -> @5004 -> 4を返却

変数コピー比較

var a = 10;
var b = a;

var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

b = 15;
obj2.c = 20;

console.log(a); //10
console.log(b); //15
console.log(obj1); //{ c: 20, d: 'ddd' }
console.log(obj2); //{ c: 20, d: 'ddd' }
console.log(a === b); //false
console.log(obj1 === obj2); //true

変数に値を割り当てるには「基本型は値を、参照型はアドレスをコピーする」と知られているが、厳密に言うとどのタイプの値であってもアドレスをコピーする。 ただし、「参照型は可変値だ」という時の「可変」は参照型データ自体を変更する場合ではなく内部のプロパーティを変更する時のみ成り立つ命題だ。

var obj1 = {a: 1, b: 'abc'};
var obj2 = obj1;
console.log(obj1 === obj2); //true
obj1 = {a: 1, b: 'abc'};
console.log(obj1 === obj2); //false

このようにobj1に同じプロパーティを持つオブジェクトを割り当てると obj1obj2は違うアドレスを指しているので===演算の結果はfalseになる。

不変なオブジェクト

渡されたオブジェクトを変更するが元のオブジェクトはそのままにしたい場合がけっこう頻繁にある。 参照型データのプロパーティを変更したい場合、新しいオブジェクトを生成するように規則を作ったりライブラリ(e.g.,immutable.js)を活用することで不変性を確保することができる。

var player = {
  name: 'Lionel Messi',
  club: 'FC Barcelona'
};

var changeClub = function (player, newClub) {
  var newPlayer = user;
  newPlayer.club = newClub;
  return newPlayer;
}

var player2 = changeClub (player, 'Machester City F.C.');

console.log(`AS-IS: ${player.club}, TO-BE: ${player2.club}`);
//AS-IS: Machester City F.C., TO-BE: Machester City F.C.

参照型データのプロパーティは可変性を持つため、Lionel Messiの履歴が前職も現職もMachester City F.C.になってしまった。

問題解決(1):オブジェクト生成

新しいオブジェクトを生成するようにハードコーディングしよう。

var player = {
  name: 'Lionel Messi',
  club: 'FC Barcelona'
};

var changeClub = function (player, newClub) {
  return {
    name: user.name,
    club: newClub
  };
}

var player2 = changeClub (player, 'Machester City F.C.');

console.log(`AS-IS: ${player.club}, TO-BE: ${player2.club}`);
//AS-IS: FC Barcelona, TO-BE: Machester City F.C.

この方法はプロパーティの数が多ければ多いほど入力する手間がかかるし、可読性が下がるので望ましくない。

浅いコピー

まずは浅いコピーを使ってオブジェクトをコピーしプロパーティを変える。

var player = {
  name: 'Lionel Messi',
  club: 'FC Barcelona'
};

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
}

var player2 = copyObject(player);
player2.club = 'Machester City F.C.';

console.log(`AS-IS: ${player.club}, TO-BE: ${player2.club}`);
//AS-IS: FC Barcelona, TO-BE: Machester City F.C.

問題解決(2):深いコピー

浅いコピーの致命的な問題は、コピーする時参照型データが持つプロパーティのアドレスだけをコピーすることにある。 下のコードではplayer.status.expiryを変えようとしている。

var player = {
  name: 'Lionel Messi',
  status: {
    age: 33
    expiry: 'June 2021'
  }
};

var player2 = copyObject(player);
player.status.expiry = 'September 2020';

console.log(`AS-IS: ${player.status.expiry}, TO-BE: ${player2.status.expiry}`);
//AS-IS: September 2020, TO-BE: September 2020

また元のデータを変更してしまう結果になった。 この不具合を防ぐには、参照型データが持つ内部の参照型データまでコピーするようにしないといけない。

var copyObjectDeep = function(target) {
  var result = {};
  if (typeof target === 'object' && target !== null) {
    for (var prop in target) {
      result[prop] = copyObjectDeep(target[prop]);
    }
  } else {
    result = target;
  }
  return result;
}

このように再帰でオブジェクトの内部プローパティまでコピーすると、playerplayer2が共有するプロパーティがないため、互い影響しなくなる。

var player2 = copyObjectDeep(player);
player.status.expiry = 'September 2020';

console.log(`AS-IS: ${player.status.expiry}, TO-BE: ${player2.status.expiry}`);
//AS-IS: June 2021, TO-BE: September 2020

また、JSONを使って深いコピーする方法もある。

var copyObjectViaJSON = function (target) {
  return JSON.parse(JSON.stringify(target));
}
var player2 = copyObjectViaJSON(player);
player.status.expiry = 'September 2020';

console.log(`AS-IS: ${player.status.expiry}, TO-BE: ${player2.status.expiry}`);
//AS-IS: June 2021, TO-BE: September 2020