便利なjavascriptテクニック集

今回はjavascriptで知ってる人にとっては当たり前だけど、そうでない人は全然知らないようなテクニックをまとめていきます。

globalを汚さない無名関数スコープ

javascriptの問題のひとつとしてスコープがよくとりあげられますが、ここではグローバルスコープを汚さない手法を紹介していきます。

悪い例

1
2
// これはグローバルスコープ
var foo = 'bar';

ちなみにglobalスコープとはwindowオブジェクトにプロパティを追加していくことと同義です。

1
2
3
// 下記の2つは結果として同じことを行っている。
var foo = 'bar';
window.foo = 'bar';

解決するには下記のように無名関数を用います。

1
2
3
4
5
// 無名関数をつくり即時実行
;(function() {
    // スコープが関数内で閉じる。
    var foo = 'bar';
}.call(this));

また、jQueryを使うときは、下記のように引数でjQueryを渡して、$という名前で引数として受け取ってあげると、$のconflict問題から開放されます。
というか$をグローバル参照するのはバグの元になるのでやめましょう。

1
2
3
4
5
;(function($) {
    // スコープが関数内で閉じる。
    var foo = 'bar';
    $('#hoge').text('hogehoge')
}.call(this, jQuery));

ちなみに無名関数の書き方は他にもあります。気にしなきゃいけないほどの違いはないので好みで選んでよいでしょう。

1
2
3
4
5
6
7
// カッコの位置がちょっと違う。
;(function() {
}).call(this);

// jslintだとsyntaxエラーになるかも。一応一文字少ないので一番軽い。
;!function() {
}.call(this);

※1 先頭のセミコロンはconcatした場合に、エラーになる可能性を排除するためです。
※2 callを使っているのは、通常の関数呼び出しだとstrictモードの場合に無名関数内部のthisがwindowオブジェクトにならないためです。

ArrayっぽいけどArrayじゃないObjectをArrayにする方法

document.getElementsByClassNameとかdocument.querySelectorAllとかで複数のDOMを取得したときの戻り値とか、argumentsプロパティは見た感じArrayっぽいのにArrayにはなってません。
その結果何がどうなるかというと、pushとかjoinとかArrayが持っているメソッドが使えず、非常に使いづらい状態です。
それを解消するための方法が下記のテクニックです。

1
2
3
4
5
// リストのDOMを取得
var list = document.querySelectorAll('.hoge-list li');

// ArrayっぽいObjectからArrayに変換
list = Array.prototype.slice.call(list);

ちょっと解説すると、まずArray型が持っているメソッドで、配列の一部を切り出すsliceメソッドがあります。
これは非破壊的メソッドで、このメソッドを実行したArrayはそのままで、実行結果を新しいArrayとして返却してくれます。
その結果引数を指定しなければ、Arrayをシャローコピー(1階層のみコピー)することができます。
さらにその特性を活かして、sliceをArrayのprototypeから直接呼び出し、callを使ってthisをArrayっぽいObjectで上書くことで完全なArray型にキャストすることができます。

関数のthisを指定したObjectでbind

当たり前に使われてますが、知らない人も結構多いかと思うので一応。applyとcallです。
javascriptのfunction型はprototypeにcallとapplyってメソッドを持っていて、呼び出したfunctionのthisを第一引数でbindすることができます。

1
2
3
4
5
6
function hoge(foo) {
    console.log(this.hoge, foo);
    // -> hogehoge bar
}
// hoge関数のthisに文字列’hogehoge’を指定して実行。
hoge.call({hoge: 'hogehoge'}, 'bar');

jQueryでthisを参照するとDOMオブジェクトになってるのはこういう方法を使ってます。そのせいでthisってなんぞ?みたいなことになるわけですが。
callとapplyの違いは関数の引数の指定方法です。
callは第2引数が第1引数に、第3引数が第2引数に、といった具合に順番がひとつずつずれる方法になります。
applyは第2引数をArray指定する形になっていて、それが順番に引数になります。
apply使うと$.whenみたいなArrayの引数ではなく可変長な引数の関数が格段に使いやすくなるのでお勧めです。

可変長引数の扱い方

関数で可変長な引数に対応したい場合がたまにあります。その方が綺麗に見える場合だったり、プラグインを独自拡張したい場合だったり。
そんなときに使えるのargumentsプロパティです。

1
2
3
4
5
6
7
8
9
function hoge(name1, name2) {
    console.log(name, name2);
    // -> foo bar

    // arumentsプロパティには引数が入っている
    console.log(arguments[0], arguments[1]);
    // -> foo bar
}
hoge('foo', 'bar');

ただ、実際に可変長引数を使いたいと思った場合は、for文とかでまわして処理を行いたいことが多いです。
しかし、argumentsプロパティは配列っぽい要素であって配列ではありません。というわけで、上で書いた手法がよく使われることになります。

1
2
3
4
5
6
7
function hoge() {
    var args = Array.prototype.slice.call(arguments);

    args.forEach(function(value)) {
        // 引数それぞれに対しての処理
    }
}

ちなみにargumentsプロパティは実装がヤバイらしく、あまり使用が推奨されていません。
ES6からは正式に可変長引数がサポートされるので将来的にはそっちを使うほうがよいです。
あとこれ必要ない場合に使うと可読性が著しく落ちるのでご利用は計画的に。
最後に、プラグイン内部で関数どおしが何やってるかわからないけど、一部の値をちょっと書き換えたり、取得したいって場合がまれによくあります。
そんなときは下記のようなテクニックが使えます。

1
2
3
4
5
6
7
8
9
// 元の処理を退避
var _ajax = Backbone.ajax;

Backbone.ajax = function() {
    // なんか独自処理

    // 元々のBackbone.ajaxを本来呼ばれたときと同じ形で呼び出す。
    _ajax.apply(this, arguments);
};

このときはBackboneのajaxをカスタマイズして、エラーハンドリングを全処理で共通的に行うようなことをしたんですが、長くなるので今回は見送ります。

argumentsの隠された力

argumentsプロパティは上で書いたようによく引数取得で使われるわけですが、実は隠された力を持っています。
ただその力はあまりにチートで危険すぎるため、strictモードでは禁止されていますのであらかじめご了承ください。

arguments.callee

まず一つ目はarguments.calleeです。これはその関数自身のことです。無名関数だけど再帰処理がしたい場合とかに使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var deepCopy = function(obj) {
    var name,
        ret;

    if(typeof obj === 'object') {
        ret = {};
        for(name in obj) {
            if(typeof obj[name] === 'object') {
                ret[name] = arguments.callee(obj[name]);
            } else {
                ret[name] = obj[name];
            }
        }
    } else {
        ret = obj;
    }

    return ret;
};

上記はObjectをdeepコピーする関数ですが、無名関数で作っているため再帰処理をする場合に自分自身を関数名で呼び出すことができません。
そのため、arguments.calleeで自分自身を呼んでいます。
もちろん名前付き関数として定義すれば問題なく動作しますが、関数名変えたりする場合に一箇所修正すればいいので個人的には好きな書き方です。
あと@azu_reさんからご指摘もらった件について追記します。
IE8を無視すれば名前付き関数式がサポートされてるので下記のような書き方が可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var deepCopy = function dc(obj) {
    var name,
        ret;

    if(typeof obj === 'object') {
        ret = {};
        for(name in obj) {
            if(typeof obj[name] === 'object') {
                ret[name] = dc(obj[name]);
            } else {
                ret[name] = obj[name];
            }
        }
    } else {
        ret = obj;
    }

    return ret;
};

// IE8に対応した書き方は下記のような感じ
var deepCopy = function (obj) {
    return dc(obj);

    function dc(obj) {
        var name,
            ret;

        if(typeof obj === 'object') {
            ret = {};
            for(name in obj) {
                if(typeof obj[name] === 'object') {
                    ret[name] = dc(obj[name]);
                } else {
                    ret[name] = obj[name];
                }
            }
        } else {
            ret = obj;
        }

        return ret;
    }
};

arguments.callee.caller.arguments

次に紹介するのはarguments.callee.caller.argumentsです。
これはなんと呼び出し元の関数の引数を取得することができます。ちなみにarguments.callee.callerは呼び出しの関数が取得できます。

1
2
3
4
5
6
7
8
9
function hoge(name) {
    hogehoge();
}
function hogehoge() {
    // 呼び出し元の一つ目の引数を表示
    console.log(arguments.callee.caller.arguments[0]);
    // -> foo
}
hoge('foo');

ただしこれをやってしまうと、可読性が著しく悪化するばかりか、関数の呼び出し順に過度に依存することになるので、非常事態以外には触るな危険です。
また、もちろんこれどんどん遡って行くことが可能で、一番最初の呼び出し元関数まで全部取得可能です。
私はBackbone.ajaxでBackbone.syncの引数の取得に使用しました。ごめんなさい。