slowjet

is a part of a carburetor

$.extend()とディープコピーを理解しよう

軽めのjQuery Advent Calendar 2012 16日目

Backbone.jsでattributesにオブジェクトを入れてハマった、っていうエントリーを書こうとしたら、ハマった僕を助けてくれたほかちゃんが先にBackbone.js Advent Calendarでエントリーを書きやがった書いてくれちゃったりしちゃったので、書くことがなくなった。

まあでも結局関連してるのって$.extend()のディープコピーだけなんで、あんまりよくわかってない人向けに書いておきます。書いてて結局これは誰向けの記事なんだろうと思い始めました。。

$.extend()って何ができんの

例えば

$.extend(a, b)

とかすると、aがbのこともできるようになります
具体的に

var shino = {
  name: 'shino',
  age: 0,
  cry: function() {
    alert('wahwahwah');
  },
  ...
};

しのちゃんは生まれたばっかりなんですね。
赤ちゃんなので泣いたりしかできません。

さて、しのちゃんは1歳になりました。
1歳になったしのちゃんは成長して挨拶ができるようになりました。

var 1YearOld = {
  age: 1,
  greeting: function() {
    alert('hello!');
  },
  ...
};

$.extend(shino, 1YearOld); 

shino.greeting();
// hello!

そんな感じです。

$.extend({}, a, b) みたいなやつを見かけました。これは何してるの

じゃあたまに見かける次のコードはなんなのっていう。

var c = $.extend({}, a, b);

最初に空のオブジェクトっぽいのが入ってるやつ。

$.extend(a, b);

さっきのこれだとaを直接拡張しちゃうから、さっきの例だとしのちゃんは1歳になってしまって、0歳ではなくなる。0歳のしのちゃんは0歳なんだし、そのまま置いておきたいと。(意味不明)つまり、aはもとのaとして置いておきたいときは

var c = $.extend({}, a, b);

とします。最初の引数に空のオブジェクトを指定すると、空のオブジェクトに対してaとbで拡張するようなイメージです。

シャロー(浅い)コピーとディープ(深い)コピー

何が浅くて、何が深いのという。
オブジェクトっていうのはそのプロパティがまたオブジェクトだったりもするわけでして、しのちゃんは16歳を過ぎてバイクの免許を取りました。バイクの免許を取ったら交通ルールとか学んでバイクのエンジンかけたり運転できるようになったりします。だいたい。

var motorcycleLicense = {
  hasMotorcycleLicense: true,
  drivingMotorcycleTechnique: {
    startTheEngine: function() {},
    drive: function() {},
    ...
  },
  ...
};

$.extend(shino, motorcycleLicense);

学んで覚えることは学校によって多少違ったりするんで、教えてもらえないこともあったりします。なので、その学校で学んだことがしのちゃんにコピーされないといけないんですけど、今、上で行ったのはシャローコピーというやつで、なんとmotorcycleLicenseの2階層目以下のオブジェクトの内容、例えばdrivingMotorcycleTechnique以下の内容が変わると、コピーした先のしのちゃんの情報まで書き換わってしまいます。これはよくない。

学校の教官が変わって、押しがけ(*1)も教えてくれるようになって、drivingMotorcycleTechniqueに押しがけ(startTheEngineWithPressing)というメソッドが追加されるとします。

*1) 押しがけっていうのは、エンジンかけるときにスタータースイッチしかないようなバイクだと、バッテリーが逝ったときに、スタータースイッチが使えなくなっちゃうんで、クラッチをつないだ状態でバイクを押しながらクランク回して、強引に点火させてしまおうという、ハードなエンジンのかけ方です。

...
$.extend(shino, motorcycleLicense); // 免許取得
motorcycleLicense.drivingMotorcycleTechnique.startTheEngineWithPressing = function() {} // 後から追加した  

console.log(shino.drivingMotorcycleTechnique);
=>
{
  startTheEngine: function() {},
  startTheEngineWithPressing: function() {}, // 覚えてないのに覚えてる
  drive: function() {},
}

そんなことができれば、しのちゃんの情報は常に自分の力以外でアップデートされるので、そんなこと出来るならやってくれ。

これはよくないというか、意図していないので、そのときにこういう第2階層以下のオブジェクトまでコピーしきってしまう方法が、ディープコピーです。やり方は簡単、第一引数にtrueを渡すだけ。

$.extend(true, shino, motorcycleLicense);
motorcycleLicense.drivingMotorcycleTechnique.startTheEngineWithPressing = function() {} // 後から追加した  

console.log(shino.drivingMotorcycleTechnique);
=>
{
  startTheEngine: function() {},
  drive: function() {},
}

後から追加した押しがけは入ってません。
つまり第二階層以下にオブジェクトがある場合は、ディープコピーにしておかないと、意図していない動作になったりしますので、気をつけましょう、という、話でした。

これほかちゃんが書いたやつと同じや!