JavaScriptコールバック

概要: このチュートリアルでは、同期コールバックと非同期コールバックを含むJavaScriptコールバック関数について学びます。

コールバックとは

JavaScriptでは、関数はファーストクラス市民です。したがって、関数を別の関数に引数として渡すことができます。

定義上、コールバックとは、後で実行するために別の関数に引数として渡す関数のことです。

以下は、数値の配列を受け取り、奇数の新しい配列を返すfilter()関数を定義しています。

function filter(numbers) {
  let results = [];
  for (const number of numbers) {
    if (number % 2 != 0) {
      results.push(number);
    }
  }
  return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
console.log(filter(numbers));Code language: JavaScript (javascript)

仕組み。

  • まず、数値の配列を受け取り、奇数の新しい配列を返すfilter()関数を定義します。
  • 次に、奇数と偶数の両方を含むnumbers配列を定義します。
  • 3番目に、filter()関数を呼び出して、numbers配列から奇数を取り出し、結果を出力します。

偶数を含む配列を返したい場合は、filter()関数を変更する必要があります。filter()関数をより汎用的で再利用可能にするために、次のことができます。

  • まず、ifブロックのロジックを抽出し、それを別の関数でラップします。
  • 次に、その関数を引数としてfilter()関数に渡します。

以下は更新されたコードです。

function isOdd(number) {
  return number % 2 != 0;
}

function filter(numbers, fn) {
  let results = [];
  for (const number of numbers) {
    if (fn(number)) {
      results.push(number);
    }
  }
  return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];
console.log(filter(numbers, isOdd));Code language: JavaScript (javascript)

結果は同じです。ただし、引数を受け取りブール値を返す任意の関数を、filter()関数の2番目の引数に渡すことができます。

たとえば、次のようにfilter()関数を使用して偶数の配列を返すことができます。

function isOdd(number) {
  return number % 2 != 0;
}
function isEven(number) {
  return number % 2 == 0;
}

function filter(numbers, fn) {
  let results = [];
  for (const number of numbers) {
    if (fn(number)) {
      results.push(number);
    }
  }
  return results;
}
let numbers = [1, 2, 4, 7, 3, 5, 6];

console.log(filter(numbers, isOdd));
console.log(filter(numbers, isEven));Code language: JavaScript (javascript)

定義上、isOddisEvenはコールバック関数またはコールバックです。filter()関数は関数を引数として受け入れるため、高階関数と呼ばれます。

コールバックは、次のような名前のない関数である匿名関数にすることができます。

function filter(numbers, callback) {
  let results = [];
  for (const number of numbers) {
    if (callback(number)) {
      results.push(number);
    }
  }
  return results;
}

let numbers = [1, 2, 4, 7, 3, 5, 6];

let oddNumbers = filter(numbers, function (number) {
  return number % 2 != 0;
});

console.log(oddNumbers);Code language: JavaScript (javascript)

この例では、個別の関数を使用する代わりに、匿名関数をfilter()関数に渡しています。

ES6では、次のようにアロー関数を使用できます。

function filter(numbers, callback) {
  let results = [];
  for (const number of numbers) {
    if (callback(number)) {
      results.push(number);
    }
  }
  return results;
}

let numbers = [1, 2, 4, 7, 3, 5, 6];

let oddNumbers = filter(numbers, (number) => number % 2 != 0);

console.log(oddNumbers);Code language: JavaScript (javascript)

コールバックには、同期コールバックと非同期コールバックの2種類があります。

同期コールバック

同期コールバックは、コールバックを使用する高階関数の実行中に実行されます。isOddisEvenは、filter()関数の実行中に実行されるため、同期コールバックの例です。

非同期コールバック

非同期コールバックは、コールバックを使用する高階関数の実行後に実行されます。

非同期とは、JavaScriptが操作の完了を待たなければならない場合、待機中に残りのコードを実行することを意味します。

JavaScriptはシングルスレッドのプログラミング言語であることに注意してください。コールバックキューとイベントループを介して非同期操作を実行します。

リモートサーバーから画像をダウンロードし、ダウンロード完了後に処理するスクリプトを開発する必要があると仮定します。

function download(url) {
    // ...
}

function process(picture) {
    // ...
}

download(url);
process(picture);Code language: JavaScript (javascript)

ただし、リモートサーバーから画像をダウンロードするには、ネットワーク速度と画像のサイズに応じて時間がかかります。

以下のdownload()関数は、setTimeout()関数を使用してネットワークリクエストをシミュレートします。

function download(url) {
    setTimeout(() => {
        // script to download the picture here
        console.log(`Downloading ${url} ...`);
    },1000);
}Code language: JavaScript (javascript)

そして、このコードはprocess()関数をエミュレートします。

function process(picture) {
    console.log(`Processing ${picture}`);
}Code language: JavaScript (javascript)

次のコードを実行すると

let url = 'https://javascripttutorial.dokyumento.jp/pic.jpg';

download(url);
process(url);Code language: JavaScript (javascript)

次の出力が得られます。

Processing https://javascripttutorial.net/pic.jpg
Downloading https://javascripttutorial.net/pic.jpg ...Code language: JavaScript (javascript)

process()関数がdownload()関数の前に実行されるため、これは期待どおりではありません。正しい順序は次のとおりです。

  • 画像をダウンロードし、ダウンロードが完了するのを待ちます。
  • 画像を処理します。

この問題を解決するには、process()関数をdownload()関数に渡し、ダウンロードが完了したらdownload()関数内でprocess()関数を実行できます。次のように。

function download(url, callback) {
    setTimeout(() => {
        // script to download the picture here
        console.log(`Downloading ${url} ...`);
        
        // process the picture once it is completed
        callback(url);
    }, 1000);
}

function process(picture) {
    console.log(`Processing ${picture}`);
}

let url = 'https://wwww.javascripttutorial.net/pic.jpg';
download(url, process);Code language: JavaScript (javascript)

出力

Downloading https://javascripttutorial.dokyumento.jp/pic.jpg ...
Processing https://javascripttutorial.dokyumento.jp/pic.jpgCode language: JavaScript (javascript)

これで、期待どおりに動作します。

この例では、process()は非同期関数に渡されるコールバックです。

非同期操作後にコード実行を続行するためにコールバックを使用する場合、コールバックは非同期コールバックと呼ばれます。

コードをより簡潔にするために、process()関数を匿名関数として定義できます。

function download(url, callback) {
    setTimeout(() => {
        // script to download the picture here
        console.log(`Downloading ${url} ...`);
        // process the picture once it is completed
        callback(url);

    }, 1000);
}

let url = 'https://javascripttutorial.dokyumento.jp/pic.jpg';
download(url, function(picture) {
    console.log(`Processing ${picture}`);
}); Code language: JavaScript (javascript)

エラー処理

download()関数はすべて正常に動作することを前提としており、例外は考慮していません。以下のコードでは、成功ケースと失敗ケースをそれぞれ処理するために、2つのコールバック(successfailure)が導入されています。

function download(url, success, failure) {
  setTimeout(() => {
    console.log(`Downloading the picture from ${url} ...`);
    !url ? failure(url) : success(url);
  }, 1000);
}

download(
  '',
  (url) => console.log(`Processing the picture ${url}`),
  (url) => console.log(`The '${url}' is not valid`)
);
Code language: JavaScript (javascript)

コールバックのネストと破滅のピラミッド

3つの画像をダウンロードして順番に処理するにはどうすればよいでしょうか。一般的なアプローチは、次のようにコールバック関数内でdownload()関数を呼び出すことです。

function download(url, callback) {
  setTimeout(() => {
    console.log(`Downloading ${url} ...`);
    callback(url);
  }, 1000);
}

const url1 = 'https://javascripttutorial.dokyumento.jp/pic1.jpg';
const url2 = 'https://javascripttutorial.dokyumento.jp/pic2.jpg';
const url3 = 'https://javascripttutorial.dokyumento.jp/pic3.jpg';

download(url1, function (url) {
  console.log(`Processing ${url}`);
  download(url2, function (url) {
    console.log(`Processing ${url}`);
    download(url3, function (url) {
      console.log(`Processing ${url}`);
    });
  });
});
Code language: JavaScript (javascript)

出力

Downloading https://javascripttutorial.dokyumento.jp/pic1.jpg ...
Processing https://javascripttutorial.dokyumento.jp/pic1.jpg
Downloading https://javascripttutorial.dokyumento.jp/pic2.jpg ...
Processing https://javascripttutorial.dokyumento.jp/pic2.jpg
Downloading https://javascripttutorial.dokyumento.jp/pic3.jpg ...
Processing https://javascripttutorial.dokyumento.jp/pic3.jpgCode language: JavaScript (javascript)

スクリプトは完全に正常に動作します。

ただし、このコールバック戦略は、複雑さが大幅に増大すると、うまく拡張できません。

コールバック内で多数の非同期関数をネストすることは、破滅のピラミッドまたはコールバック地獄として知られています。

asyncFunction(function(){
    asyncFunction(function(){
        asyncFunction(function(){
            asyncFunction(function(){
                asyncFunction(function(){
                    ....
                });
            });
        });
    });
});
Code language: JavaScript (javascript)

破滅のピラミッドを回避するには、Promiseまたはasync/await関数を使用します。

概要

  • コールバックとは、後で実行するために別の関数に引数として渡される関数のことです。
  • 高階関数とは、別の関数を引数として受け入れる関数のことです。
  • コールバック関数は、同期または非同期にすることができます。
このチュートリアルは役に立ちましたか?