JavaScript
JavaScriptCore

JavaScript中級者への道【5. コールバック関数】

More than 1 year has passed since last update.

JavaScript中級者への道【5. コールバック関数】

JavaScriptのつまづきやすそうなところ

コールバック関数とは

既に「関数の引数に関数が渡せる」ということを学びました。
これを利用して、「ある特定の処理が終わったら、引数に渡した関数の処理を実行する」といったように
処理のフローを制御することが出来ます。(というより、非同期の場合は制御する必要があります)
その際、引数に渡される関数のことを「コールバック関数」といいます。

Wikipediaのコールバック (情報工学)を見る限り、同期/非同期関係無く、引数に関数を取る実装を指しているようです。
ですが、JavaScript界隈で「コールバック関数」といえば、非同期処理に関する記事が多く見受けられます。

まずは制御とか云々の前に、単純なコールバック関数の実装から見てみましょう。

単純な実装

// コールバック関数を実行する関数
function execCallback (callback) {
  console.log('I call callback');
  callback();
}

// execCallback()に渡されるコールバック関数
var myCallback = function () {
  console.log('This is my callback');
}

// execCallback()にコールバック関数を渡して実行する
execCallback(myCallback);  // => 'I call callback'
                           //    'This is my callback'

callback();のところで引数のコールバック関数を実行しています。
ところで、この例ではvar myCallback = function () {..}という関数オブジェクトを事前に準備しましたが、
これは以下のような形でコールバック関数を無名関数に書き換えることも出来ます。

// execCallback()に無名のコールバック関数を渡す
execCallback(function () {
  console.log('This is anonymous callback');  // => 'I call callback'
});                                           //    'This is anonymous callback'

上記のように、その場で関数オブジェクトを生成してすぐに引数で渡すような実装は一般的です。

前回、非同期関数について自分で実装する場合はsetTimeout()を使うということを学びました。
実はこの時、何の説明もなく、しれっとコールバック関数を利用していました。

setTimeout(function () {
  console.log('hello');
}, 3000);

非同期関数の話に持っていってしまった為、あまり取り上げませんでしたが、
この関数の本来的な目的は「指定した秒数後に、引数に渡した関数(処理)を実行する」というものです。

コールバック関数を使ってやりたいこと

  1. 非同期処理の中で、決まった順序で処理を実行したい
  2. 関数を渡す形にすることで、非同期処理を実行した後の処理の内容を自由に決めたい

という2点です。
先ほど挙げた例では、全て同期処理として動作する

コールバックを使わない場合

前回の失敗例の再掲です。非同期の呼び出しと実行のタイミングの違いから、
hogeがundefinedになってしまうという例でした。

コールバック関数を使用しない実装1
var hoge;

// 0秒後にhogeに文字列を代入する
setTimeout(function () {
  hoge = 'hoge';
}, 0);

console.log(hoge);  // => undefined

これは生で書いてしまっているので、まずは関数に直してみます。

コールバック関数を使用しない実装2_関数化
function showHoge () {
  var hoge;
  setTimeout(function () {
    hoge = 'hoge'
  });
  console.log(hoge);
}

showHoge();  // => undefined

取り敢えずshowHogeという関数に包んでみました。setTimeoutの内部でhoge='hoge';の代入が実行される前に
console.log(hoge);が実行されるので、まだundefinedのままです。
これを解決する為に、setTimeout()の内部にconsole.log(hoge);を移してみます。

function showHoge () {
  var hoge;
  setTimeout(function () {
    hoge = 'hoge'
    console.log(hoge);
  });
}

showHoge();  // => `hoge`

undefinedが解消されました。ただ、このままだとこの関数はconsole.log(hoge)を行うだけです。
次に、コールバック関数を渡して、渡した関数を実行するような実装に変更してみましょう。

コールバックを使った場合

コールバック関数を使用した実装
// hogeに文字列が代入された後にコールバック関数を実行する関数
function hogeWithCallback (callback) {
  var hoge;
  setTimeout(function () {
    hoge = 'hoge'
    callback(hoge);
  });
}

// 引数をログに出力する関数
var putLogParam = function (param) {
  console.log(param);
}

// ログを出力するコールバック関数を渡す
hogeWithCallback(putLogParam);  // => 'hoge'

// 引数をアラートに表示する無名関数をコールバック関数として渡す
hogeWithCallback(function (param) {
  window.alert(param);  // => 'hoge'
});

コールバック関数を利用することで、非同期処理の順序を制御し、処理の内容を可変にすることが出来ました。
実は、上記の実装は何の説明も無く二つステップアップしています。お気付きでしょうか。

function hogeWithCallback (callback) {
  var hoge;
  setTimeout(function () {
    hoge = 'hoge'
    callback(hoge);  // ← コールバック関数に引数を渡して実行している
  });
}

この部分です。何が言いたいのかというと、コールバック関数には引数を渡すことが出来ます。

コールバック関数(無名関数)
// paramにhogeが入ってくる
hogeWithCallback(function (param) {
  window.alert(param);  // => 'hoge'
});

知ってないと感じる気持ち悪さ

個人的な経験ですが、JavaScriptを触り始めて間もない頃、
以下のような書き方にとてつもない気持ち悪さを感じていました。

jQuery#ajax
$.ajax({
   type: "POST",
   url: "some.php",
   data: "name=John&location=Boston",
   success: function (msg) {
     alert( "Data Saved: " + msg );
   }
 });

これとか、

fs#readFile(node.js)
fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

これのことです。

何が気持ち悪いかというと、コールバック関数の「msg」とか「err, data」とかの引数って、
一体、どこのどいつがいつ入れてるんやねんということです。

誰がデータを入れているのかを想像してみる

先ほど「非同期関数」 + 「コールバック関数」 + 「引数を渡せる」ということを学びました。
ということで、単純化すれば以下のような形で書けます。

非同期関数を単純化してみる
// 第一引数に入力、第二引数にコールバック関数を受け取る関数
// 入力を元に非同期にデータを取得し、コールバック関数に渡して実行する
function asyncFunc (input, callback) {
  setTimeout(function() {
    // setTimeoutを使って、非同期にしたい時間がかかる処理を行う
    // URLからHTTPでデータを取ってきたり、パスからファイルを読み込んだりなど
    var data = getData(input);
    // 何かしらの方法でエラーがあったという情報を受け取っておく(無ければnull)
    var err = data.getError();
    // コールバック関数の第一引数にエラー情報、第二引数にデータを渡して実行する
    callback(err, data);
  }, 0);
}

// 第一引数に入力、第二引数にコールバック関数を渡す
// コールバック関数の第一引数にはエラー情報、第二引数には取得したデータが入ってくる
asyncFunc('/pass/to/file', function (err, data){
  if (err) throw err;
  console.log(data);
});

ライブラリの実装は見ていないので、あくまで想像の範疇ですが。
まぁ、コールバック関数は自分で作ること(主に無名関数で)を踏まえると、元の関数が入れるしかないのだけれど。
ちなみにgetData(input);とかdata.getError();とかは架空の関数です。

「非同期関数」+「コールバック関数」というアーキテクチャの何が嬉しいかって、
ライブラリで用意された非同期関数を使ってみるとよく分かるような気がします。
以下のように、ライブラリの開発者と利用者で関心と実装の分離が実現出来るからです。

開発者:時間がかかる処理の非同期化に集中出来る
利用者:処理時間の削減はライブラリが提供してくれるので、ビジネスロジックに集中出来る

痒いところに手が届かないとかもあったりするとは思いますが、開発者の方には人には頭が上がりません。

ただし、注意は必要

非同期関数+コールバック関数はその構文上、ネストが深くなりやすいという欠点があります。
非同期処理をシーケンシャルに行おうとすると、コードがどんどんネストしていくので、「コールバック地獄」に陥ります。
コールバック関数にビジネスロジックをガリガリ書いたり、例外処理をきちんとやろうとすると大変なことになりそうです。
(setTimeoutでthrowしたエラーはcatch出来ないらしいです。)
ここではその解決法については触れませんが、参考までに以下の記事を紹介しておきます。

[JavaScript] 非同期処理のコールバック地獄から抜け出す方法
コールバック……駆逐してやる…この世から…一匹…残らず!!
JavaScript Promiseの本
JavaScriptと非同期のエラー処理

あくまで「コールバック地獄」が問題なのであって、非同期処理が連続するようなことが無ければ
素直にコールバック関数を利用することは良い選択だと思われます。

それと、JavaScript中級者への道【2. 4種類のthis】でもちらっと述べた通り、
コールバック関数にオブジェクトのメソッドを渡す際は注意が必要で、
いつの間にかthisがグローバルオブジェクトを指してしまうケースがあります。

オブジェクトのメソッドをコールバック関数として渡す
var obj = {
  name: 'matsuby',
  hello: function () {
    console.log(this.name);
  }
};

function useCallback (callback) {
  callback();
}

useCallback(obj.hello);  // => undefined

このような場合、bindを使ってメソッドのthisをobjに固定してあげましょう。

bindを使ってthisをobjに固定
useCallback(obj.hello.bind(obj));  // => 'matsuby'

まとめ

  • コールバック関数は非同期処理の実行順序を制御出来る
  • コールバック関数を引数に取る関数を実行する時、引数に無名関数を渡すのは一般的なやり方
  • コールバック関数はただの関数なので、引数を取ることが出来る
  • コールバック関数の引数に実際に値を渡しているのは、コールバック関数を引数に取る関数の方である
  • ライブラリの非同期関数を使うことで、利用者は非同期になる処理を意識することなく、ビジネスロジックが書ける
  • コールバック関数にオブジェクトのメソッドを渡す場合はbindを使ってthisを固定する

その他

JavaScriptは構文的には簡単な言語かもしれませんが、概念的には非常に難しい言語だと思います。
JavaScriptに限った話でもないとは思いますが、「A」という概念を実装するために
「B」の知識を知っておく必要があるという知識の連鎖をとみに感じます。(大抵は「関数」絡みですけどね。)
取り上げるテーマは、そういった知識の連鎖の順序を考えて書いていたりします。

ところで「コールバック関数を引数に取る関数」の名前ってあるんでしょうか。
もし知っている方がいらっしゃれば教えていただければと思います。m(_ _)m
以上。