Subterranean Flower

JavaScriptの「コールバック関数」とは一体なんなのか

Author
古都こと
ふよんとえんよぷよぐやま!!!

近年のフロントエンドの盛り上がりはすごいですね。プログラミング初心者がJavaScript(最近ではTypeScriptも)から学び始めるなんて昔ではなかなか考えられなかったことです。

そんな世界中で大人気のJavaScriptですが、プログラミングに慣れていても困惑する部分が結構あります。特に初心者にとっては、非同期処理、this、コールバック関数、あたりが難しいのではないかと思います。

非同期処理については前に解説しましたし、thisの解説はネットに大量に転がってるので、今回はコールバック関数について解説します。

コールバック関数ってなんなんだ

コールバック関数というとsetTimeoutなんかで使われるアレですね。Node.jsでもたくさん使われます。setTimeoutだと以下のような使い方になります:

setTimeout(function() { console.log('Hello!'); }, 2000);

これで2秒後に「Hello!」と表示されます。これがコールバック関数です。いったいなんなんですかね、これ。

コールバック関数については、だいたい次のような解説がされることが多いです。

「別の関数に呼び出してもらうための関数」

ええと、つまり?どういうこと?

こいつ、「僕はJavaScriptの基礎的な機能ですよ」みたいな顔しつつ、実は結構難しい要素が絡んできます。なので、ちょっと遠回りしつつコールバック関数への道を辿っていきましょうか。

値と関数

プログラミングをしたことがあるなら「値」という言葉は馴染み深いと思います。プログラミングにおいては数値はもちろん「値」ですし、文字列も「値」です。trueとfalseも値です。配列も値です。オブジェクトも値です。だいたい全部値ですね。

値は変数に突っ込んだり操作したりできます。

const numValue = 100;
const strValue = "私は値です!";
const boolValue = true;
const arrayValue = [1, 2, 3];
const objValue = { key: 'value' };

変数に値を割り当てていろいろ操作して目的の動作を実現する、というのがプログラミングの基本でしたね。

ここで話は変わって「関数」というものもあります。関数は値を受け取って何か処理をして値を返すやつでしたね。

// 関数定義する
function add(a, b) {
  return a + b;
}

// 使う
console.log(add(1, 2)); // 3が表示される

この「関数」ですが、JavaScriptにおいてはちょっと秘密があります。実は関数は変数に突っ込めます。

// 関数を変数に割り当てる
const addFunc = function(a, b) {
  return a + b;
}

// 使う
console.log(addFunc(1, 2)); // 3が表示される

変数addFuncに関数を割り当てて、addFuncを呼び出して関数を実行してますね。マジかよ。なんだこれ。

実はJavaScriptでは「関数」も「値」です。そんな「犬も実は人間です」みたいな話をいきなり受け入れられないかもしれませんが、関数は値です。

難しくいうとJavaScriptにおいて関数は第1級オブジェクト(ファーストクラスオブジェクト)です。第1級オブジェクトというのは数値や文字列のような、変数に割り当てられたり、なんらかの計算処理ができたりといった、プログラミングの基本機能が使える対象のことを指します。JavaScriptでは変数に関数を突っ込めるので、関数は第1級オブジェクトです。

関数を値として扱うには後ろのカッコをつけずに書きます。add(1, 2)とかじゃなくて、単にaddと書きます。これを使えば、新しく作った関数だけでなく、すでに定義済みの関数でも遊べます。

// 自分で定義する関数
function add(a, b) {
  return a + b;
}

// 定義した関数を変数に入れる
const addFunc = add; // カッコはつけない!

// JavaScriptに標準でついてる関数でもできる
const myMax = Math.max; // max関数をmyMaxという変数に入れる

// 呼び出してみる
console.log(add(1, 2), addFunc(1, 2)); // どっちも3になる
console.log(Math.max(1, 2), myMax(1, 2)); // どっちも2

逆にいうと、後ろのカッコがあると関数の中の処理の呼び出しになります。カッコがあれば処理として、カッコがなければ値として、使い分けができます。

const max1 = Math.max(1, 2); // これはカッコがついてるので関数の計算結果が入る
const max2 = Math.max; // これはカッコがないので関数自体が入る

カッコ無しの場合、関数は実行されません。カッコには「関数を実行する」の意味があるので、無いと何も起こりません。

ちなみに関数以外(文字列とか)にカッコをつけて関数呼び出ししようとすると「not a function(関数じゃないよ)」と怒られます。

関数じゃないよって怒られるということは、カッコは関数の呼び出しをしていることの証明にもなりますね。

こんな風に、JavaScriptでは関数を「値」としていろんなところに引きずり回したり、勝手に別名をつけたりできます。

関数を受け取る関数

関数は値である。そんなことを聞いたらちょっと悪さをしたくなりますね。

例えば先ほどの関数addを思い出してください。addは値を受け取り、計算結果を返します。関数は「値」を受け取れますからね。そしてもうひとつ思い出してほしいのですが、関数は「値」です。つまり、こういうことができます:

関数に関数を渡せる

ええぇ……。この事実により、以下のような関数を作ることができます:

// 関数を2回実行する関数!!
function doTwice(func) {
  func(); // 1回目!
  func(); // 2回目!
}

// あいさつするだけの関数
function hello() {
  console.log('Hello!');
}

// あいさつを2回実行する
doTwice(hello);

関数doTwiceは受け取った関数を2回実行するだけの関数です。引数funcに関数が入ります。そしてこのdoTwiceに「Hello!」と表示するだけの関数を渡します。渡すときはhelloのカッコを外すのを忘れずに!doTwice自体は関数として実行するのでカッコが必要です。すると2回実行され、「Hello!」が2回表示されます。

このような、「関数を受け取る関数」を「高階関数」と呼びます。

なお、渡す関数にいちいち名前をつける必要はなく、functionほにゃららって記述を直接渡せます。数値とかを変数に入れずに「1」とか「2」で関数に渡せるのと同じですね。

// 関数を2回実行する関数!!
function doTwice(func) {
  func(); // 1回目!
  func(); // 2回目!
}

// あいさつを2回実行する
doTwice(function() {
  console.log('Hello!');
});

何か見たことのある形です。どんどん真相に近づいてきた気がしますね。

加えて、変数に入った関数にも値を渡して実行できます。さっきのaddFuncとかで、もうやりましたけども。

// 関数に値を渡しつつ2回実行する関数!!
function doTwiceWithValue(func) {
  func('Hello!'); // 1回目!
  func('I am here!!!'); // 2回目!
}

// 受け取ったmessageを表示するだけの関数を渡す
doTwiceWithValue(function(message) {
  console.log(message);
});

この例ではdoTwiceWithValue関数は受け取った関数に、1度目は「Hello!」を渡して実行、2度目は「I am here!!!」を渡して実行します。

そしてこの関数に引数messageをひとつ取る関数を渡します。するとmessageの中に「Hello!」が入って実行され、次に「I am here!!!」が入って実行されます。つまり「Hello!」と「I am here!!!」が続けて表示されます。

「関数を受け取る関数」と「引数をとる関数」のペア、かなり大事なので覚えておきましょう。

コールバック関数

そろそろ本題に戻りましょうか。コールバック関数についてです。

コールバック関数ですが、広い定義でいうと、単なる「高階関数に渡すための関数」です。さっきのhelloとかfunction(message)とかはコールバック関数になりますね。そんだけです。はい。自分で直接実行するのではなく、相手に実行してもらうのがコールバック関数です。

setTimeoutで考えてみましょう。setTimeoutは、受け取ったコールバック関数を指定ミリ秒後に実行します。

setTimeout(function() {
  console.log('Hello!');
}, 2000);

これで「2000ミリ秒後にこのfunctionを実行して!」ということになります。関数が値であることと高階関数のことを知った後だと簡単に理解できますね。要はsetTimeout(func, ms)という高階関数を使っているだけです。

それだけ!おわり!……と行きたいところですがJavaScriptではちょっとややこしい事情があり……。

非同期処理とイベントとコールバック関数

JavaScriptでコールバック関数が使われるのって大抵は非同期処理なんですよね。さっきのsetTimeoutも非同期処理ですし。

非同期処理というのは我々が信じる「プログラムは書いた順に動く」という基本を無視した、「今書かれたけど後で実行するから先に進んで」ってやつです。許さん。詳しくは「Promiseとasync/awaitでJavaScriptの非同期処理をシンプルに記述する」で書いているので、そちらもあわせて読んでみてください。

非同期処理は「後で」行われるので、順番を記述することが難しくなります。たとえばsetTimeoutの後にメッセージを表示しようとして以下のように書いても無駄です:

setTimeout(function() { console.log('Hello!');}, 2000);
console.log('Bye!!!!');

この例だと「Bye!!!! Hello!」と表示されます。「Hello! Bye!!!!」と表示したいんですが……。

そんなこんなで、「非同期処理の後に何か実行したい」ってことがよくあります。特に「ボタンがクリックされた」とか「読み込みが完了した」とかのときに何かやりたいことは多いはずです。

そこで悪魔の発想です。「自分で非同期処理の終わりを待つなんて難しいことせずに、非同期処理自身に後処理実行させればいいんじゃね?」と思いついた奴がいます。責任の押し付けです。ここで、「処理」は「関数」という単位でまとめることができます。そして関数は値です。つまり、非同期処理に関数を値として渡して、後で実行してもらうようにすれば、みんなハッピーになれるわけです。

JavaScriptの多くの非同期処理は、これを以下のような方法で実現しています:

  1. 非同期処理関数はコールバック関数を受け取る高階関数にする
  2. 利用者は「終わったら実行したい処理」をコールバック関数として渡す
  3. 非同期処理関数は処理が終わったらコールバック関数を呼び出す

setTimeoutなんかがまさにこれですね。関数を渡しておいて、あとで実行してもらう、俺はもう知らん。というやつです。

JavaScriptでコールバック関数というと、だいたいこの非同期処理のコールバック関数のことを意味します。非同期処理だけでもややこしいのにそこにコールバック関数も絡んできたら、そりゃややこしくなって混乱しますよね。

この手法はDOMのイベントなんかでも使われます。例えば「ボタンクリックされたらコンソールにclickedと表示する」処理を書くときは以下のようにしますよね:

document.querySelector('.my-button').addEventListener('click', function(event) {
  console.log('clicked!');
});

ずいぶんややこしく見えますが、addEventListener(eventName, func)という単純な高階関数で、eventNameに対してfuncを登録するというだけのシンプルな作りです。前に言った通り関数はそのまま値として渡せるので、直接function(event)ほにゃららと書いてるだけです。

もちろんコールバック関数は関数名で渡すこともできます。

function callback(event) {
  console.log('Hello'!);
}

document.querySelector('.my-button').addEventListener('click', callback);

どちらもほぼ同じなんで好きな方でやってください。

注意点は、ここでは関数を登録するだけで、実行してないところです。つまりaddEventListenerをした時点ではfunc()をしていないということです。内部ではclickListener.push(func)みたいなことをして配列にfuncを追加してるだけのイメージです。それでクリックされたときにはじめてfunc(event)を実行しています。

// こういうイメージです

const clickCallbackList = []; // 空の配列

function addEventListener(eventName, callback) {
  if(eventName === 'click') { // クリックイベントなら
    // 配列にcallback登録。実行はしない
    // 実行するときはcallback(event)ってやる
    clickCallbackList.push(callback);
  } else if(eventName === 'load') {
     // 省略
  }
}

doTwiceの例では関数を渡した瞬間に実行していましたが、JavaScriptの一般的な非同期処理は「とりあえず渡されたら自分の持ってる変数に登録だけして実行は後でやる」というのが多いです。

実行タイミングが後にずれるだけでかなりわかりにくくなりますが、やってること自体は上の方のdoTwiceやdoTwiceWithValueとそう変わらないです。

Node.jsと非同期処理とコールバック関数

Promiseの普及もあってコールバック関数を見る機会も割と減ったと思いますが、Node.jsの世界ではまだまだ非同期コールバックが残っています。そう簡単にライブラリの作りを変えられないからです。でも「コールバック関数を渡さなければPromiseを返す」みたいな作りもよくありますね。

メジャーどころのライブラリならPromise対応してたりしますが、ちょっとマイナーめなのになるといまだにコールバック前提の作りしてたりするので、もうちょっとだけコールバックと付き合うことになりそうです。

requestとか使うと以下のような書き方しますよね:

request('http://www.google.com', function (error, response, body) {
  console.log(error);
  console.log(response);
  console.log(body);
});

これもrequest(url, func)の高階関数になってて、レスポンスが返ってきたらその内容とともにfunc(error, response, body)を実行しているだけですね。高階関数という概念を知っていると一気に簡単になります。

余談

余談ですが、コールバック関数の引数名は固定だと思ってる人がそこそこいます。固定じゃないです。数と順番さえあってれば名前はなんでもいいです。requestの例だとfunction(e, r, b)とかでも大丈夫です。

もっというと引数の数があってなくても動きます。JavaScriptさんそのあたりあんまり気にしない性格なので……。requestの例だとfunction(error)とか渡すとerrorだけ入って実行され、function(error, response, body, a, b, ,c, d)とか渡すとerrorとresponseとbodyだけ入ってあとのaとかbとかはundefinedになります。

このあたり気楽でもありバグの原因でもあるので、気をつけましょう。

まとめ

  • JavaScriptにおいて関数は「値」である
  • 関数は「値」を受け取れる
  • つまり関数に関数を渡すことができる(高階関数)
  • コールバック関数とは、高階関数に渡す関数のことである
  • JavaScriptではコールバック関数は非同期処理とともに用いられることが多い