今回はコールバック関数のお話です
こんにちは、江嵜です。
JavaScript 書いてますか!
書いてないですか?
JavaScript は若干、 他の言語と比べてとっつきにくい部分があったりする言語だと思われているように思います。
実際、今でこそ JavaScript は非常に人気な言語ですが、
元々ガッツリプログラミングをするための言語ではなく、
ブラウザ上で簡単な処理をさせるために作られた言語なんですね。
ということで、Java のようなしっかりとしたプログラムを組むことを考えて作られた言語を触られた方からしてみると
使いにくい!良くわからない!と思うところが結構あったりするのではないでしょうか。
例えばコレ。
setTimeout(function() { console.log('hello!'); }, 1000);
処理を遅らせる時に使う setTimeout
ですが、この時に使われるのが、
引数の所に関数を書く「コールバック関数」!
なんでこんな書き方になるのか、考えたことありますか?
今回は、いまいち意味が分からないまま使われていることが多そうな(実際私も昔はそうでした)
コールバック関数について解説!してみましょう。
その前に…簡単に JavaScript のオブジェクトと関数についてのお話
オブジェクトリテラルは値をまとめたオブジェクト
その前に、軽く JavaScript のオブジェクトと関数の立ち位置についてお話しましょう。
オブジェクトリテラル、という言葉はもしかしたら聞きなれないかもしれませんが、
JavaScript を使ったことがある方ならほぼオブジェクトリテラルも使ったことがあるでしょう。
オブジェクトリテラルとは、値を中カッコでまとめたヤツです。
var obj = { val1: 'hoge', val2: 'fuga' }
こんな感じのやつです!
今回は作ったオブジェクトリテラルを obj 変数に格納しているので、
console.log(obj.val1) // 「hoge」 と出力される
こんな感じで使いますよね。
ここで重要なのは、「オブジェクトリテラルはオブジェクトであり、変数に格納できる」ということです。
関数とは複数の処理をまとめたオブジェクト
JavaScript について勉強するときにはお世話になる MDN から解説を引いてみましょう。
関数は JavaScript の基本的な構成要素のひとつです。また関数は、JavaScript の手続き ― つまり、タスクや値計算を実行する文の集まりです。関数を使うには、呼び出したいスコープ内のどこかでそれを定義する必要があります。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Functions
なるほど…?
これだけ読むと分かるようで分からないような解説ですが、ポイントはここ
タスクや値計算を実行する文の集まりです。
単純に実行する文をまとめたもの、という認識です。
ちなみに文とは処理の 1 ステップです。
ですから例えば、
function add (a, b) { var result = a + b; console.log('合計値は' + result + 'です'); return a + b; }
これは、
- 与えられた二つの数を足して
- 合計値をコンソールに出力し
- 合計値を返す
という処理をまとめて、 add
という名前を付けたもの、ということになります。
さらに、知っておいていただきたのは、関数もオブジェクトであるということです。
先程オブジェクトリテラルは値をまとめたオブジェクトであるとお話しましたが、
関数は処理をまとめたオブジェクトなのですね!
どちらもオブジェクトということは…?
関数もオブジェクトなので変数に格納できる
この辺りから Java 等とは少し異なり、少し理解しづらいところではありますが…
オブジェクトリテラルも関数も、何かをまとめたオブジェクトであることをお話しました。
ところで先程、オブジェクトリテラルは変数に格納できることをお見せしましたよね?
関数もオブジェクトですから、オブジェクトリテラル同様に変数に格納することができます。
よって、
var add = function (a, b) { var result = a + b; console.log('合計値は' + result + 'です'); return a + b; }
こんな書き方が可能です。
つまり、 先程の add
と同じ関数(オブジェクト)を作って、それを add
変数に格納しているのですね。
ここまで OK ですね?
関数は引数を渡すと発動する
さて、
- 関数は単純に処理をまとめたオブジェクトである
- 関数は変数に格納できる
ということをお話しました。
そうすると、一つ疑問が生まれてくるかと思います。
「変数に格納した関数はどうやって実行するんだ?」
ということですね。
これは単純で、関数には引数を与えてあげることで実行ができます。
では、先ほどの add 関数を実行してみましょう。
引数の与え方は関数の後ろに (値)
をくっつけてあげれば OK です。
つまり、
add(2, 3)
こうですね。
関数を変数に格納している場合はどうでしょうか。
こちらもまったく同じように、変数名の後ろに (値)
をくっつけてあげれば OK です。
なぜなら、変数は呼び出されたタイミングで中身を展開しますので、
add(2, 3)
は
(function (a, b) { var result = a + b; console.log('合計値は' + result + 'です'); return a + b; })(2, 3)
と同じですね!(add 変数の中身の塊を示す為に () でくくってあります)
これは即ち、関数の後ろに (値)
をくっつけていることになりますので、
これで関数を実行できる、というわけです!
この形、どこかで見た覚えがありませんか?
そう、関数を作ってすぐ実行させる、即時関数ですね!
(function (){ console.log('hello world!') })()
即時関数も一緒で、関数の後ろに引数を与えてあげているだけだったのです。
ただ、このように関数が引数を何も取らない場合は ()
と、かっこの中身が空になるだけで、
関数 + 引数を渡す形に従っていただけなのですね!
いよいよコールバック関数
さて、ここまで長い旅でしたね。
いよいよコールバック関数について考えてみましょう!
setTimeout(function() { console.log('hello!'); }, 1000);
元々、こんな形の関数を考えていましたよね。
それでは、この setTimeout 関数をどのようにしたら作れるか、考えてみましょう。
(以下は本当の setTimeout の実装とは異なります。コールバック関数をどのように実現しているか、という点について見てください)
まず、 setTimeout
を定義する必要があるので、
function setTimeout(fn, time) { }
こんな関数定義を考えましょう。
引数としては一つ目にコールバック関数を、二つ目に遅延させる時間を入れます。
ということは、
function setTimeout(fn, time) { // sleep(time) : time に指定された時間だけ処理を待つ // 関数実行 }
こんな感じになれば OK ということですね
(JavaScript には sleep 関数はありません。あくまでイメージですよ)
関数実行は引数を与えてあげればよかったので、
function setTimeout(fn, time) { // sleep(time) : time に指定された時間だけ処理を待つ fn() }
としましょうか。
こうすると、すなわち最初の
setTimeout(function() { console.log('hello!'); }, 1000);
これは
function setTimeout(fn, time) { // sleep(1000) -> 1000 ミリ秒だけ処理を待つ (function() { console.log('hello!'); })() }
こうなります。
関数の後ろに引数を与えているので、確かに順番として
- 1000 ミリ秒待つ
- コールバック関数を実行
の順が守られていますね!
すなわち、コールバック関数とは JavaScript の関数がオブジェクトであり、
変数に代入できるという特性を生かした書き方だった、ということですね!
実際に理解してしまえばなんてことないコールバック関数ですので
皆さんも是非使ってみてくださいね!
オマケ:関数を返す関数
ちなみに、関数がオブジェクトであるということは、当然関数の返り値を関数にすることだって OK です。
例えば、
var exec = function () { console.log('実行!'); }
こんな感じで何かしらを実行する関数があったとして、
var logger = function(fn) { return function () { console.log('関数実行開始'); return fn } }
こんな風にすると…
logger(exec)()
とすることで
(function(fn) { return function () { console.log('関数実行開始'); return fn } })(exec)()
一つ目の関数が実行されて
function () { console.log('関数実行開始'); return exec }
が返ってくると
(function () { console.log('関数実行開始'); return exec })()
になって、これを実行すると最終結果は
- 関数実行開始
- 実行!
と出力されます。
これはいわゆるデコレーターですね!
こんな感じで、関数を返却させることで
fu()()
みたいな、かっこが複数つながった形が発生することもあります。
ですが、結局は全て今回解説したルールに従っているだけなので、
落ち着いてみれば理解できるものになっています!
JavaScript は慣れると柔軟に書ける言語なので、皆さんも楽しんでコーディングしてくださいね!