JavaScript アドバンス

JavaScript コールバック

「後でかけ直すよ(Call back later)!」
コールバック(Callback)とは、後で実行される関数(Function)のことです。
何か特定の処理が完了した後に実行されます。

「コールバック」という名前は、タスクが終わったときにその関数があなたを「呼び戻す(Call you back)」というアイデアに由来しています。かつてのJavaScriptにおいて、すぐには得られない非同期(Asynchronous)の結果を扱うための最初の解決策がこれでした。

このページでは、コールバックとは何か、そしてなぜそれが時に問題を引き起こすのかを解説します。

1. コールバック関数とは何か?

コールバック関数とは、別の関数に引数(Argument)として渡される関数のことです。
この関数は、通常、特定のイベントが発生したときや非同期オペレーションが完了したときなど、後で実行されることを目的としています。

このパターンを採用することで、関数の柔軟性を高く保つことができます。これは、古いJavaScript APIの多くが採用していた仕組みです。

2. イベントハンドリング (Event Handling)

コールバックは、JavaScript、特にイベントハンドリング(Event Handling)において頻繁に使用されます。
ボタンのクリックやキー入力などのユーザーインタラクションは、イベントリスナー(Event Listener)にコールバック関数を提供することで処理されます。

document.getElementById("myButton").addEventListener("click", displayDate);

上記の例では、displayDateaddEventListener() メソッドに引数として渡されたコールバック関数です。
ユーザーが id="myButton" のボタンをクリックしたときに、displayDate が呼び出されます。

3. 非同期オペレーション

setTimeout() のようなブラウザの関数は、指定された遅延の後にコードを実行するためにコールバックを使用します。

setTimeout(myFunction, 3000);

function myFunction() {
  document.getElementById("demo").innerHTML = "大好きです!!";
}

上記の例では、myFunctionsetTimeout() に引数として渡されたコールバック関数です。
3000 は、myFunction が呼び出されるまでのミリ秒数です。

関数を引数として渡すときは、括弧 () を使用しないことを忘れないでください。
正しい: setTimeout(myFunction, 3000)
間違い: setTimeout(myFunction(), 3000)

4. タイミングの問題

非同期コードは後から完了します。
つまり、処理が終わる前に結果をすぐに返す(Returnする)ことはできません。

let result;

setTimeout(function() {
  result = 5;
}, 1000);

// ここでの result は何でしょうか?

結果は undefined になります。非同期コードがまだ完了していないためです。
JavaScriptにおいて、この問題を「待機(スリープ)」によって解決することはできません。待機させるとページ全体がフリーズしてしまいます。

5. コールバックの概念 (The Callback Idea)

上記の問題に対する解決策は、結果の準備が整った後にコードを実行することです。
そのために、後で呼び出してもらうためのコールバック関数をJavaScriptに渡す必要があります。

コールバックは関数から別の関数を呼び出すことを可能にするテクニックです。

function done(value) {
  myDisplayer(value);
}

setTimeout(function() {
  done(5);
}, 1000);

// 値はコールバックの中で利用されます

コールバックは後で実行されるため、これで期待通りに動作します。

6. シーケンス制御 (Sequence Control)

関数の実行タイミングをより精密に制御したい場合があります。
例えば、計算を行ってからその結果を表示したいとします。

まずは、計算関数 myCalculator を呼び出し、その後に表示関数 myDisplayer を呼び出す方法です:

// 何かを表示する関数
function myDisplayer(some) {
  document.getElementById("demo").innerHTML = some;
}

// 合計を計算する関数
function myCalculator(num1, num2) {
  let sum = num1 + num2;
  return sum;
}

// 計算機を呼び出す
let result = myCalculator(5, 5);

// 表示器を呼び出す
myDisplayer(result);

あるいは、myCalculator を呼び出し、その関数の中から myDisplayer を呼び出す方法もあります:

function myDisplayer(some) {
  document.getElementById("demo").innerHTML = some;
}

function myCalculator(num1, num2) {
  let sum = num1 + num2;
  myDisplayer(sum);
}

// 計算機を呼び出す
myCalculator(5, 5);

1つ目の例の問題は、結果を表示するために2つの関数を呼び出す必要がある点です。
2つ目の例の問題は、計算関数が結果を表示することを防げない(表示処理が強制される)点です。

ここで、コールバックの出番です。

コールバックを使用すると、myCalculator にコールバック(myCallback)を渡し、計算が終わった後にそのコールバックを実行させることができます。

例(コールバックの活用)

function myDisplayer(some) {
  document.getElementById("demo").innerHTML = some;
}

function myCalculator(num1, num2, myCallback) {
  let sum = num1 + num2;
  myCallback(sum);
}

// myDisplayer をコールバックとして渡す
myCalculator(5, 5, myDisplayer);

この例では、myDisplayer がコールバック関数として使用され、myCalculator() の引数として渡されています。

7. コールバックのエラーハンドリング

非同期コードは失敗することもあります。
コールバックでは、エラー優先(Error-first)パターンがよく使われます。

function getData(callback) {
  let ok = true;

  if (ok) {
    callback(null, "データ");
  } else {
    callback("何かが失敗しました", null);
  }
}

getData(function(error, data) {
  if (error) {
    myDisplayer(error);
    return;
  }
  myDisplayer(data);
});

このパターンでは、コールバック関数の最初のパラメータがエラー、2番目のパラメータが結果になります。これは古いJavaScriptコードで非常によく見られる標準的な書き方です。

8. コールバックの欠点 (Callback Drawbacks)

JavaScriptプログラミングにおいて不可欠なコールバックですが、深くネスト(入れ子)されると、コードが複雑で読みづらくなります。これは「コールバック地獄(Callback Hell)」や「死のピラミッド(Pyramid of Doom)」と呼ばれています。

step1(function(r1) {
  step2(r1, function(r2) {
    step3(r2, function(r3) {
      console.log(r3);
    });
  });
});

コールバックが深くなると、デバッグが極めて困難になります。ロジックが左から右へと移動してしまい、流れを追うのが難しくなるためです。

9. コールバックに代わる手段

非同期コールバックの解決策は、書くのもデバッグするのも大変です。
そのため、モダンな非同期JavaScriptではコールバックを多用しません。

現在のJavaScriptは、よりクリーンなフローと優れたエラーハンドリングを提供する Promises(プロミス)や Async/Await 構文という優れた代替手段を提供しています。

10. いつコールバックを使うべきか?

それでも、コールバックを理解することは非常に重要です。
コールバックが真価を発揮するのは、ある関数が別の関数の完了を待たなければならない(ファイルの読み込み完了を待つなど)非同期関数においてです。

基礎をしっかり固めておくことで、最新の非同期パターンもより深く理解できるようになりますよ。