Mach3.laBlog

“extend” – Alphabetical Advent Calendar 2013

この記事は賞味期限切れです。(更新から1年が経過しています)

“X” は extend の “X”。

X

extend

JavaScript で extend というと、幾つかのオブジェクトをマージして拡張する処理の事として知られます。
かなり頻繁に使われるこの機能は、多くのJavaScriptフレームワーク・ライブラリに備えられています。

例えば jQuery の場合は $.extend として実装されています。

var dest = {a: 1, b: 2};
var src = {a: 2, c: 3};

$.extend(dest, src);
console.log(dest); // {a: 2, b: 2, c: 3} 

$.extend は複数のオブジェクトを引数に渡し、先頭に渡したオブジェクトの拡張をします。
その他のフレームワークにおいても基本的な使い方は共通で、
underscore.js の _.extend もほぼ同様の記述で利用できます。

いくつかのパターン

extend 機能を実装する上で、いくつかのパターンが考えられます。

  • 値がオブジェクトだった場合に再帰的にコピー(ディープコピー)するか否か
  • 既に同じ名前のプロパティが存在した場合に上書きするか否か

これらは正解不正解なくケースによって使い分ける物だと思いますが、
利用する extend 関数がどのパターンで機能を提供するのかは把握しておいた方が良いでしょう。
特に、ディープコピーの有無によるミスは気づきにくいかもしれません。

ちなみに jQuery はディープコピーの有無をコントロールする事が出来、underscore.js はディープコピーを行いません。
同名プロパティについては両者ともに後から渡された値で上書きを行います。

ディープコピーと参照

コピー元のプロパティの値がオブジェクトであった場合、
それをそのままコピーしてもそれは元のオブジェクトへの参照になります。
例えば次のように src のプロパティに data というオブジェクトがぶら下がっているとします。

var dest = {};
var src = {
    data: {
        a: 1,
        b: 2
    }
};

この src を元にして dest を拡張してみます。

$.extend(dest, src);
console.log(dest); // {"data":{"a":1,"b":2}} 

問題なく拡張出来たように見えますが、
ここでコピー元の src.data のプロパティを変更してみると、それが dest.data にも反映される事が分かります。

src.data.a = 0;
console.log(dest); // {"data":{"a":0,"b":2}} 

これは dest.data が src.data に紐付いている為に引き起こる様です。
この時 dest.data を src.data から切り離した新しいオブジェクトとして複製したい場合は、ディープコピー を使います。
jQuery の場合は引数の先頭に true を足してあげることでディープコピーが出来ます。

$.extend(true, dest, src);

同名プロパティの上書き

あまりないケースかもしれませんが、
コピー先のプロパティを活かしたい場合は引数の最後に渡して改めてコピーする事になります。

$.extend(dest, src, {
    propToProtect: dest.propToProtect
});

新しいオブジェクトとして生成する

継承した結果を新しいオブジェクトとして取得したい場合は、
空のオブジェクトを先頭に渡します。

var obj = $.extend({}, src1, src2);

src1 / src2 共にプロパティの変更はされず、空のオブジェクトに継承された結果が返り値として返ります。

extend の習作

なんとなく extend の働きを理解したところで、簡単な extend 関数を習作してみます。
まずはシンプルにディープコピー無しの extend です。

var extend = function(/* dest, src1, src2 */){
    var i = 0, dest, src, name;
    dest = arguments[i++];
    for(; i<arguments.length; i++){
        src = arguments[i];
        for(name in src){
            if(! src.hasOwnProperty(name)){ continue; }
            dest[name] = src[name];
        }
    }
    return dest;
};

次に、ディープコピーの有効・無効をコントロール出来るようにしたタイプです。
(コントロールをなくして別名関数にしてしまった方がシンプルに済むかもしれません)

// obj のタイプを文字列で返す
var type = function(obj){
    var m = Object.prototype.toString.call(obj).match(/\[object\s(\w+)\]/);
    return m ? m[1].toLowerCase() : null;
};

var extend = function( /* [deep,] dest, src [, src2 ...] */ ){
    var i = 0, deep = false, dest, merge;
    if(type(arguments[0]) === "boolean"){
        deep = arguments[i++];
    }
    dest = arguments[i++];
    merge = function(deep, dest, src){
        var name;
        for(name in src){
            if(! src.hasOwnProperty(name)){ continue; }
            if(deep && type(src[name]) === "object"){
                dest[name] = extend(deep, dest[name] || {}, src[name]);
                continue;
            }
            dest[name] = src[name];
        }
    };
    for(; i<arguments.length; i++){
        merge(deep, dest, arguments[i]);
    }
    return dest;
};

jQuery等がない状況でコードを書く時や、
好みの動作を行う extend 関数が欲しい時には自作のスニペットを用意しておくと捗るかもしれませんね。

参考資料

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*