【JavaScript】コールバック関数とは?わかりやすく解説!

JavaScriptを勉強していると、「コールバック関数(callback function)」という言葉をよく目にします。

この記事では『コールバック関数』について、以下の内容をサンプルコードを用いてわかりやすく解説します。

  • コールバック関数とは?
  • コールバック関数がよく使われる場面
    • 非同期処理(setTimeoutなど)
    • 配列操作(forEach, map, filterなど)
    • イベントリスナー
  • コールバック関数の様々な書き方
    • 通常の関数定義
    • 無名関数(名前のない関数)
    • アロー関数(ES6以降)
  • コールバック関数のよくある間違い
  • 引数付きのコールバック関数
  • コールバック地獄(callback hell)とは

コールバック関数とは?

コールバック関数は、「別の関数に引数として渡される関数」です。

コールバック関数は、あとで別の関数から呼び出される(コールバックされる)ことを目的として渡されます。

まずはコールバック関数ではない「通常の関数」のサンプルコードを以下に示します。以下のサンプルコードでは、sayHello関数を自分で呼び出しています。

function sayHello() {
  console.log("こんにちは!");
}

sayHello(); // 実行結果:こんにちは!

次に、「コールバック関数」を使ったサンプルコードを以下に示します。

function greet(callback) {
  console.log("はじめまして!");
  callback(); // 引数として受け取った関数を呼び出す
}

function sayHello() {
  console.log("こんにちは!");
}

greet(sayHello);
// 実行結果:
// はじめまして!
// こんにちは!

ここでは、sayHello関数をgreet関数の中で「後から呼び出される(コールバックされる)」ために、引数として渡しています。これが「コールバック関数」です。

ここで、コールバック関数のイメージ図を以下に示します。

コールバック関数とは?

コールバック関数は以下の流れで動作します。

  • 関数Aの引数にコールバック関数Bを渡して、関数Aを呼び出す
  • 関数A内で引数として受け取ったコールバック関数Bを呼び出す
  • コールバック関数Bが実行される

関数Bは関数Aの引数に渡されているので、コールバック関数となります。

コールバック関数がよく使われる場面

コールバック関数は、「ある処理の後に実行したい処理」を指定するためによく使われます。たとえば、非同期処理(例: setTimeoutやAPI通信)の後」や「配列の繰り返し処理」などで使われます。

非同期処理(setTimeoutなど)

非同期処理では、処理が終わった後に実行したい関数をコールバックとして渡す場面がよくあります。

setTimeout(function () {
  console.log("3秒後に実行されました");
}, 3000);

ここで渡している無名関数(function)は、3秒後に自動的に呼び出されるコールバック関数です。setTimeoutの中では「今はまだ実行しないけど、時間が経ったら呼び出してね」という使い方をしています。

配列操作(forEach, map, filterなど)

配列を操作するメソッドの多くも、各要素に対して行う処理をコールバック関数として渡すのが基本です。

const fruits = ["りんご", "バナナ", "みかん"];

fruits.forEach(function (fruit) {
  console.log(fruit);
});

このforEachに渡している無名関数も、配列の各要素に対して順番に呼び出されるコールバック関数です。他にも mapfilter など、コールバック関数を使う配列メソッドはたくさんあります。

イベントリスナー

ユーザーの操作(クリックやキー入力など)に応じて処理を実行したい場合にも、コールバック関数が使われます。

たとえば、ボタンがクリックされたときに特定の処理を実行するには、addEventListenerメソッドを使い、イベント発生時に実行したい関数をコールバックとして登録します。

document.getElementById("btn").addEventListener("click", function () {
  console.log("ボタンがクリックされました");
});

この例では、無名関数(function () { ... })がコールバック関数です。「クリックされたらこの関数を実行してね」とブラウザに伝えておくことで、実際にクリックされたタイミングで関数が呼び出されます。

コールバック関数の様々な書き方

コールバック関数は、目的や使う場面に応じて、いくつかの異なる書き方ができます。ここでは、代表的な3つの書き方を紹介します。それぞれの特徴を知っておくと、状況に応じた書き方が選べるようになります。

通常の関数定義

関数を別で定義しておき、それを引数として渡すパターンです。

function callbackFunc() {
  console.log("実行されました!");
}
someFunction(callbackFunc);

この方法は、処理を何度も再利用したい場合に便利です。

無名関数(名前のない関数)

使い捨ての関数をその場で定義して渡す方法です。

someFunction(function () {
  console.log("無名関数が実行されました!");
});

名前を付ける必要がない処理に対して、コードの可読性や記述量を減らすのに便利です。

アロー関数(ES6以降)

ES6以降では、アロー関数(=>)を使って、より簡潔に記述することもできます。

someFunction(() => {
  console.log("アロー関数が実行されました!");
});

アロー関数は短く書けるので、短いコールバック処理に適しています

コールバック関数のよくある間違い

以下のコードを見てみましょう。

function greet(callback) {
  console.log("はじめまして!");
  callback(); // 引数として受け取った関数を呼び出す
}

function sayHello() {
  console.log("こんにちは!");
}

greet(sayHello()); // これは間違い!!

これはNGです。sayHello()と書くと、その場でsayHello関数が実行されてしまい、実行結果(undefined)がgreetに渡されてしまいます。正しくは、関数そのものを渡すために括弧をつけずにsayHelloと書く必要があります。

greet(sayHello); // 正解!!

引数付きのコールバック関数

コールバック関数に引数を渡したいときは、ちょっとした工夫が必要です。

次のサンプルコードを見てみましょう。

function greet(callback) {
  console.log("はじめまして!");
  callback(); // ← ここで渡された関数を実行
}

function sayHello(name) {
  console.log(`こんにちは、${name}!`);
}

このとき、sayHello("田中さん")greet()にコールバックとして渡したくなります。そこで以下のように書くのはNGです。

greet(sayHello("田中さん")); // これはダメ!

このコードでは、sayHello("田中さん")がすぐに実行され、その「戻り値(undefined)」がgreetに渡ってしまいます。

正しい書き方を以下に示します。

greet(() => sayHello("田中さん"));

このように「引数付きの関数をコールバックにしたいときは、無名関数やアロー関数でラップしてから渡す」ことで、greetcallback()を呼び出すタイミングで、sayHello("田中さん")が実行されるようになります。

なお、greet関数がnameを引数として受け取る設計になっていれば、以下のように「引数付きの関数を直接呼び出す形」で自然に書けます。

function greet(name, callback) {
  console.log("はじめまして!");
  callback(name);
}

function sayHello(name) {
  console.log(`こんにちは、${name}!`);
}

greet("田中さん", sayHello);

コールバック地獄(callback hell)とは

コールバック地獄(Callback Hell)」とは、コールバック関数が入れ子(ネスト)になりすぎて、コードの可読性や保守性が著しく低下する状態を指します。以下に、JavaScriptでの典型的な「コールバック地獄」の例を示します。

console.log("処理開始");

setTimeout(() => {
  console.log("1秒後の処理");

  setTimeout(() => {
    console.log("さらに1秒後の処理");

    setTimeout(() => {
      console.log("さらにもう1秒後の処理");

      setTimeout(() => {
        console.log("最後の処理");
      }, 1000);

    }, 1000);

  }, 1000);

}, 1000);

このように、コールバック関数が入れ子になると右にどんどんインデントが深くなり、処理の流れが非常に読みづらくなります。これがいわゆる「コールバック地獄」です。修正やデバッグが困難になるだけでなく、予期せぬバグも発生しやすくなります。

この問題を改善するために、Promiseを使った書き方に変えることができます。サンプルコードを以下に示します。

console.log("処理開始");

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

wait(1000)
  .then(() => {
    console.log("1秒後の処理");
    return wait(1000);
  })
  .then(() => {
    console.log("さらに1秒後の処理");
    return wait(1000);
  })
  .then(() => {
    console.log("さらにもう1秒後の処理");
    return wait(1000);
  })
  .then(() => {
    console.log("最後の処理");
  });

Promiseを使うことで、ネストを避けて直線的な処理の流れを表現できるようになります。これにより、可読性や保守性が大きく向上します

さらに、async/await構文を使うことで、より直感的な同期処理風のコードが書けるようになります。サンプルコードを以下に示します。

async function run() {
  console.log("処理開始");

  await wait(1000);
  console.log("1秒後の処理");

  await wait(1000);
  console.log("さらに1秒後の処理");

  await wait(1000);
  console.log("さらにもう1秒後の処理");

  await wait(1000);
  console.log("最後の処理");
}

run();

async/awaitを使うと、非同期処理であるにもかかわらず、まるで同期処理のような読みやすいコードになります。これがモダンな非同期処理の主流の書き方です。

本記事のまとめ

この記事では「コールバック関数(callback function)」について、以下の内容を説明しました。

  • コールバック関数とは:「あとで呼び出すために、別の関数に渡される関数」
  • よく使われる場面:非同期処理(setTimeoutなど)、配列操作(forEachmap)、イベントリスナー
  • 書き方のバリエーション:通常の関数定義、無名関数、アロー関数
  • よくある間違い:関数を渡すときに()を付けて実行してしまう
  • 引数付きコールバック:無名関数やアロー関数でラップして渡す必要がある
  • コールバック地獄とは:コールバックのネストが深くなりすぎて、可読性や保守性が悪化する状態
  • 改善方法:Promiseやasync/awaitを使うと、コードがすっきり読みやすくなる

お読み頂きありがとうございました。

スポンサーリンク