Edited at

JavaScriptコールバックを整理してみた【再入門】


はじめに

JavaScriptのコールバックについて、シンプルに理解できるよう整理してみました。

特に目新しいことはないかと思いますが、何かのお役に立てば幸いです。


目次


  • コールバック(関数)とは

  • 単純なコールバック関数を実装する

  • より実践的(かもしれない)コールバック関数を実装する

  • おまけ | Promiseに置き換える

  • 終わりに

  • 参考サイト


コールバック(関数)とは

ひとことで言うと、「引数として渡される関数」です。

他関数の引数として使用し、特定のタイミングで実行させることができます。「あの処理が終わった後に、この関数を実行したい」など。


単純なコールバック関数を実装する

例えばイベント発火時のコールバックは、使い方・動作がイメージしやすいかと思います。


click-callback.js

// #triggerをclickしたら、console.log()を実行 

$('#trigger').click(function(e) { // <-このfunctionがcallback
console.log(e.currentTarget);
return false;
});

// 関数を外に出すことも
var showLog= function(e) {
console.log(e.currentTarget);
return false;
};
$('#trigger').click(showLog); // <-showLogがcallback


イベント以外で、単純なコールバックを実装してみます。


simple-callback.js

/* --- 変数・関数定義 --- */

var trapCard = function() {
console.log('トラップカード発動!');
};
var sacrificeCard = function(sacrificeName) {
console.log(sacrificeName + 'を生け贄にするぜ!!');
};
// オレのターン!と表示してから、引数を実行する関数
var myTurn = function(callback) {
console.log('オレのターン!');
callback();
};

/* 呼び出し */
myTurn(trapCard);
// オレのターン!
// トラップカード発動!

/* --- コールバック関数自体に引数を渡すには以下のように書くことも --- */
/* function()を噛ませる */
myTurn(function() {
sacrificeCard('ブルーアイズホワイトドラゴン');
});

/* .bind()を使用する */
myTurn(sacrificeCard.bind(null, 'ブルーアイズホワイトドラゴン')); // 引数を束縛するだけなら第1引数はnullでok

// (上記どちらも)
// オレのターン!
// ブルーアイズホワイトドラゴンを生け贄にするぜ!!


呼び出す側に、コールバック関数とその引数を渡して対応することもできます。


simple-callback2.js

// ... 略 ...

var myTurn = function(callback, callback_arg) {
console.log('オレのターン!');
callback(callback_arg);
};

myTurn(sacrificeCard, '城之内');
// オレのターン!
// 城之内を生け贄にするぜ!!



より実践的(かもしれない)コールバック関数を実装する

複数の処理を特定のタイミングで個別に実行していくプログラムを書いてみます。

【動作イメージ】


animation-callback.js

/* --- 変数・関数定義 --- */

var $div = $('#js-div');

var a = function(callback) {
setTimeout(function() {
$div.addClass('is-right');
callback();
}, 500);
};

var b = function(callback) {
setTimeout(function() {
$div.addClass('is-top');
callback();
}, 500);
};

var c = function(callback) {
setTimeout(function() {
$div.addClass('is-red');
callback();
}, 500);
};

var end = function() {
setTimeout(function() {
$div.addClass('is-hide');
}, 500);
};

/* 呼び出し */
a(b.bind(null, c.bind(null, end.bind(null))));



  • 各関数ではdivにclassを付与しているだけです

  • ※アニメーションはcssのtransitionを使用

  • 呼び出す記述順でアニメーション処理の順番が変わります


c(b.bind(null, a.bind(null, end.bind(null))));



おまけ | Promiseに置き換える

上記プログラムでも問題なく動作するのですが、関数の数だけ呼び出し側での入れ子が多くなり、可読性・保守性が落ちてきます。いわゆるコールバック地獄というやつです。

これを解決するには、Promiseが使えます。

以下のように書き換えてみます。

※Promiseについては、ページ下部の参考サイトなどをご覧ください



  • Promiseクラスが使える前提です

  • 置き換え前のコードと比べてsetTimeoutのdelayが500->1000になってますが間違えただけなので気にしないでクダサイ(gifアニメもそのまま撮ってしまったので動きが遅いです)



animation-promise.js

/* --- 変数・関数定義 --- */

var $div = $('#js-div');

var a = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
$div.addClass('is-right');
resolve();
}, 1000);
});
};

var b = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
$div.addClass('is-top');
resolve();
}, 1000);
});
};

var c = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
$div.addClass('is-red');
resolve();
}, 1000);
});
};

var end = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
$div.addClass('is-hide');
resolve();
}, 1000);
});
};

/* 呼び出し */
a()
.then(b)
.then(c)
.then(end);


呼び出し側の関数がシンプルになりました!

処理順の操作も簡単です。


animationの順番を変えた.js

c()

.then(b)
.then(a)
.then(end);

【動作イメージ】


このように非同期でコールバックが入れ子になる場合は、素直にpromiseやdeferredを使った方が良いかと思います。書いておいて何ですが。



終わりに

ゴリゴリのajaxや非同期処理が多用されていなくとも、コールバックを意識・分離することでよりシンプルなコードが書けるかもしれません。

何か変なところや勘違いしているところがあれば、ご指摘ください!


参考サイト