applyとcallの使い方を丁寧に説明してみる
JavaScriptに、applyとcallというメソッドが用意されていますが、自分なりにapplyとcallの丁寧に説明をしてみようと思ってこのエントリーを書くなどをしてみます。
applyとcallは非常に似たメソッドなので、まずはcallから説明します。
callメソッドとは?
callメソッドは以下のように呼び出します。
methodA.call(thisArg, [, arg1 [, arg2, ...]]);
methodAには任意の関数(メソッド)を指定します。
callの引数は第一引数にmethodAのthisとしたいオブジェクトを指定して、第二引数以降はmethodAに渡したい引数があれば、カンマ区切りでそれぞれ指定します。
callメソッドは、すべての関数が共通して持っているメソッドです。すべての関数はFunctionクラスのオブジェクトで、callはFunction.prototypeの中にあります。
なので、関数宣言、関数式、new Function。いずれの方法で関数を生成した場合も、callメソッドが継承されます。applyも同じです。
callメソッドの使い方
var hoge = function() { console.log('hoge'); };
関数hogeを定義しました。関数hogeはcallメソッドを保有しています。
試しにfirebug等で確認しましょう。
console.log(hoge.call);
undefinedとはならず、関数hogeはcallメソッドを保有していることが確認できます。
では、関数hogeでcallメソッドを使ってみます。
関数hogeは引数なしの関数なので、第二引数以降は指定しません。第一引数はどうなるでしょうか。
関数hogeはグローバルオブジェクトの関数になります。JavaScriptのグローバルオブジェクトはwindowなので、第一引数はwindowを指定すれば良いということになります。
hoge.call(window);
firebugのconsoleにhogeという文字列が出力されます。
関数hogeの仕様を以下のように変更してみましょう。
var hoge = function(str_value) { console.log(str_value); };
関数hogeは引数にとった任意の文字列をconsoleウィンドウに出力する関数になりました。
この関数hogeでcallメソッドを使ってみます。
第一引数は先ほどと同様にwindowです。第二引数以降はhogeに渡したい引数を指定するので、出力したい文字列を指定しましょう。
hoge.call(window, 'fuga');
firebugのconsoleにfugaという文字列が出力されます。*1
第一引数は明示的にwindowを指定していましたが、undefinedやnullを指定すると、暗黙的にグローバルオブジェクトに変換されます。
hoge.call(null, 'fuga');
これでもOKです。
第一引数をもう少し噛み砕く
methodA.call(thisArg, [, arg1 [, arg2, ...]]);
第一引数がmethodAのthisとしたいオブジェクトと書きましたが、これ、自分で書いててもしっくりきません。
第一引数に何を指定するかを理解することが、callメソッドを理解する肝だと思うので、もう少し噛み砕いて説明します。
thisを出力する実験
firebugでthisを出力する実験をしてみましょう。
まずは単純な関数から。
var hoge = function() { console.log(this); }; hoge();
Windowが出力されます。
では、つぎにオブジェクトに定義した関数で実験してみます。
var hoge = { echo : function() { console.log(this); } }; hoge.echo();
これはWindowではないオブジェクトが出力されます。
クリックして詳細を確認すると、オブジェクトの中にechoメソッドが定義されているので、hogeオブジェクトが出力されているってことです。
では、クラスに定義した関数で実験してみます。
var Hoge = function() {}; Hoge.prototype.echo = function() { console.log(this); }; var h = new Hoge(); h.echo();
これもオブジェクトが出力されていることが確認できます。
より正確にhオブジェクトが出力されていることを確認するには以下のようにします。
var Hoge = function() {}; Hoge.prototype.echo = function() { console.log(this instanceof Hoge); }; var h = new Hoge(); h.echo();
これでtrueが表示されることで、Hogeクラスのインスタンスである、hオブジェクトがthisだったことが確認できます。
callメソッドの第一引数に指定するthisは何か
先ほどの実験結果を見ると、thisにはそのメソッドを実行しているオブジェクトが束縛されていることが確認できると思います。
なので、callメソッドの第一引数にも、あるメソッドを実行させたいオブジェクトを指定すれば良いということです。
callメソッドのすごいところ
callメソッドのすごいところは、本来使えないはずのメソッドが使えることです。
var Hoge = function() {}; Hoge.prototype.echo = function() { console.log('hoge'); }; var Fuga = function() {}; var h = new Hoge(); var f = new Fuga();
上記のようなコードがあった場合、h.echo()は使えますが、f.echo()はFugaに定義されていないので、使うことができません。でも、callメソッドにかかればこんなこと関係がなくなります。
var Hoge = function() {}; Hoge.prototype.echo = function() { console.log('hoge'); }; var Fuga = function() {}; var h = new Hoge(); var f = new Fuga(); h.echo.call(f);
FugaとしてHogeが保有しているechoメソッドを実行できてしまいます。
本当にFugaとしてメソッドを実行しているのか、thisの値を見て確認してみます。
var Hoge = function() {}; Hoge.prototype.echo = function() { console.log(this instanceof Fuga); }; var Fuga = function() {}; var h = new Hoge(); var f = new Fuga(); h.echo.call(f);
trueと出力されるので、あくまでfオブジェクトとしてechoを実行していることが確認できます。
callメソッドすごい。
callメソッドがわかりにくい理由
ここ重要
callメソッドがわかりにくい理由は主体と客体が逆になっているところなのかなと思います。
通常のメソッド呼び出しは以下のようになります。
objA.method();
これを、以下のように考えます。
主体.method();
主体とは、methodを実行させたいオブジェクトです。普通、主体はmethodを実行させたいオブジェクトであるとともに、methodを保有しているオブジェクトでもあると思います。
ところがcallメソッドになると以下のようになります。
objA.method.call(objB);
先ほどの表現に合わせると以下のようになります。
客体.methodA.call(主体);
客体とは、メソッドを実行させたいオブジェクトではなく、実際にそのメソッドを保有している第三者であるオブジェクトのことです。
メソッドを実行させたい主体ではなく、実際にそのメソッドを保有している客体から書き始め、引数にメソッドを実行させたい主体を指定します。
自分自身慣れの問題でしたが、通常の感覚で書こうとすると頭がこんがらがりますね。
よくあるcallメソッドの利用例
callメソッドの説明が終わったところで、よくあるcallメソッドの利用例を紹介します。
特にcallメソッドの客体として頻繁に利用されるのが、Array.prototypeです。Arrayオブジェクトには大量の便利なメソッドがあります。
ところが、document.getElementsByTagName('hoge')のように、DOMの要素リストを受け取った時に、見た目はArrayっぽいのにArrayのメソッドを持っていないという困った問題があります。
こういう時、callメソッドが使えます。
例えば、ページ上の全てのa要素を取得して、先頭の要素を取り出すコードを書いてみます。
var array_like_anchors = document.getElementsByTagName('a'); var anchors = Array.prototype.slice.call(array_like_anchors); anchors.shift();
要素リスト(array_like_anchors)はsliceメソッドを持っていません。なので、実際にsliceメソッドを持っている、Array.prototypeを客体として指定して、メソッドを実行したい主体である、array_like_anchorsでsliceメソッドを実行します。
sliceでコピーされた要素リストは、通常の配列になっているので、shiftで先頭の要素を取り出すことができるようになります。
他にも、関数の引数リストであるargumentsオブジェクトも上記の例と同じようにすれば、先頭を取り出すことが簡単にできるようになります。
applyの説明
applyの存在忘れてた。
applyはcallで引数をカンマ区切りで指定するのと違い、引数を単一の引数リストとして使う点が異なります。
なのでこんな感じで使えます。
var sum = function() { var result = 0; for (var i = 0; i < arguments.length; i++) { result += arguments[i]; }; return result; }; var args = [1,2,3,4,5]; sum.apply(window, args);
利用シーンとしては、可変長の引数を指定する関数を呼び出したい時に使う他、配列形式の引数の方が好ましい場合に使います。
ほとんどがcallで事足りるような気がしています。
まとめ
apply/callメソッドは、JavaScriptに「おまえのものはおれのもの、おれのものもおれのもの」というジャイアニズムを実現するための素晴らしいメソッドなのです。
*1:hoge.callはwindow.hoge.callということ