🌟

コールバック関数について調べてみる

2025/03/02に公開
3

chatGPTを使ってコールバック関数について調べてみました。

現在のレベル

「コールバック関数って、関数内で呼び出される関数のことでしょ?」

「なんかPromiseの登場によって使わてもよくなったやつ?」


(質問)コールバック関数について、初学者でも分かるように説明してください。

コールバック関数(callback function)とは、ある処理が終わった後に
「次に実行するために呼ばれる関数」のことです。
簡単に言うと、「関数の中に渡す、後で実行される関数」 です。

■コールバック関数を使う理由

通常、プログラムは上から下に順番に実行されますが、コールバック関数は、
例えば非同期処理(時間がかかる処理)をした後に別の処理を行いたい場合に使います。

例えば:

・サーバーからデータを取得した後に、取得したデータを使って別の処理をしたい。
・ユーザーがボタンをクリックした後に、特定のアクションを実行したい。

■コールバック関数の基本的な使い方

コールバック関数は、他の関数に引数として渡して、実行が完了したタイミングで呼ばれます。
function greet(name, callback) {
  console.log("Hello, " + name);
  callback();  // コールバック関数を呼び出す
}

function sayGoodbye() {
  console.log("Goodbye!");
}

greet("Alice", sayGoodbye);  // 「Hello, Alice」が表示された後、「Goodbye!」が表示される

つまり、関数の中で呼び出される関数というだけではなく、実行されるタイミング(非同期処理)をコントロールできることが重要という訳ですね。


(質問)Promiseとの関係について教えてください。

■コールバック関数とPromiseの違い

コールバック関数: 
非同期処理が完了した後に実行される関数を指定します。
非同期処理が複雑になると、コールバック関数が入れ子(ネスト)になって、
コードが見づらくなることがあります(これを「コールバック地獄」と呼びます)。
Promise: 
非同期処理を扱うためのオブジェクトで、処理が成功した場合や失敗した場合に
どのように結果を扱うかを指定します。
Promiseは、非同期処理が完了する前に値を返さない代わりに、
後で結果が返ってくることを約束(Promise)するものです。

■Promiseを使った例

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "データ取得成功!";
      resolve(data);  // データ取得成功した場合
    }, 1000);
  });
}

fetchData()
  .then(result => {
    console.log(result);  // データ取得成功!
    return "次の処理";
  })
  .then(nextResult => {
    console.log(nextResult);  // 次の処理
  })
  .catch(error => {
    console.error("エラー:", error);  // エラー処理
  });

■Promiseを使うメリット

  • 非同期処理のチェーン: .then() を使って、非同期処理の結果を順番に処理することができます。
  • エラーハンドリング: .catch() でエラーを一元的に処理できます。
  • 可読性の向上: 非同期処理の順番が明確になり、コードが読みやすくなります。

(質問)Promiseでなく、コールバック関数を使わざるを得ない場合はありますか?

解答はいくつかあったのですが、下記が重要と感じました。

■イベントリスナー(リアルタイム処理)の場合

Promiseは一度しか解決(resolve/reject)できないため、
繰り返し発生するイベントには向いていません。
イベントリスナーを使う場合は、コールバック関数が必要になります。
document.addEventListener("click", () => {
  console.log("クリックされました!");
});

なぜPromiseを使えない?

Promise は 一度しか実行されない ため、複数回のクリックを処理できません。
イベントが発生するたびに実行される処理は、コールバック関数が最適です。

■JavaScriptの一部の非同期処理(例えば、setTimeout)

setTimeout や setInterval などの組み込み関数は、コールバック関数しか受け付けません。
setTimeout(() => {
  console.log("3秒後に実行されました!");
}, 3000);

まとめ

今回はコールバック関数について調べました。

とりあえずPromiseを使っていればいいやと思っていましたが、コールバック関数が必要になるケースについても知ることができました。

次はasync/awaitとの関係についても学習したいと思いますが、長くなりそうなので一旦区切りたいと思います。

GitHubで編集を提案

Discussion

junerjuner

Promise は 一度しか実行されない ため、複数回のクリックを処理できません。イベントが発生するたびに実行される処理は、コールバック関数が最適です。

そこで asyncIterator すればいいのでは……?
Promise なら await だが、 asyncIterator なら for await ... of が使えますし。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for-await...of

ちなみに Promise.withResolvers() を使えば resolve用のコールバックと待てる promise が取得できます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers

const {resolve, promise} = Promise.withResolvers();

(async () => {
  await null:
  resolve();
})();
await promise;

例えば asyncIterator を似たように使いたいなら次の様に書けます。

const {values, resolve} = withResolvers();
// クリックの度に呼び出されるコールバックの登録
buttonElement.addEventListener('click', resolve);
// クリックイベントを実行する度に呼び出されるループとして呼び出し
for await (const event of values) {
  // DOING...
}

function withResolvers() {
  let controller;
  const stream = new ReadableStream({
    start(controller_) {
      controller = controller_;
    }
  });
  return {
    values: stream.values(),
    resolve: controller.enqueue.bind(controller),
    complete: controller.close.bind(controller),
    reject: controller.error.bind(controller),
  };
}
junerjuner

ReadableStream の values() は safari はまだなので、 gemini さんに for await が出来る状態で書き換えてもらうと次の感じになります。

function withResolvers() {
  const queue = [];
  let closed = false;
  let error = null;

  const asyncIterator = {
    [Symbol.asyncIterator]() {
      return this;
    },
    async next() {
      if (error) {
        throw error;
      }
      if (queue.length === 0 && closed) {
        return { done: true };
      }
      if (queue.length === 0) {
        await new Promise(resolve => {
          const check = () => {
            if (queue.length > 0 || closed || error) {
              resolve();
            } else {
              setTimeout(check, 0);
            }
          };
          check();
        });
      }
      return { value: queue.shift(), done: false };
    },
    return() {
      closed = true;
      return Promise.resolve({ done: true });
    },
    throw(err) {
      error = err;
      return Promise.reject(err);
    }
  };

  return {
    values: asyncIterator,
    resolve: (value) => { queue.push(value); },
    complete: () => { closed = true; },
    reject: (err) => { error = err; }
  };
}
MasatoMasato

コメントありがとうございます

まだasyncIteratorやPromise.withResolvers()が必要になるような複雑な処理は扱ったことがなく、
勉強になります。
自身のメモとして、下記を載せておきます。

(質問)asyncIterator vs Promise.withResolvers() の使い分けについて教えてください。

asyncIterator vs Promise.withResolvers() の使い分け
非同期的な繰り返しが必要な場合(例えば、ユーザーが複数回ボタンをクリックする
イベントを連続して処理する場合)には、asyncIterator の方が 自然 に動作します。
asyncIterator は非同期で繰り返し処理するのに適しており、
イベントが発生する度に順番に処理を行いたい場合に最適です。

同期的な処理が必要な場合(例えば、特定の条件を満たすタイミングで非同期処理を開始したい場合)
には、Promise.withResolvers() の方が 推奨 されます。
これにより、非同期処理の開始タイミングを手動で制御できます。
例えば、ユーザーの操作や特定のイベントが発生した時にのみ非同期処理を開始する場合などです。