本日の学習ではJavaScriptのコールバック・高階関数について学習します。
コールバック関数は、おそらくJavaScriptで最も一般的に使用される関数型プログラミング手法となります。
初学者はこのコールバックを謎のままにし、知らず知らずのうちに使用していた方もいらっしゃいます。
つまりJavaScriptには、いくつかの高階関数が組み込まれています、それらが高階関数であることに気付かずに、すでにそれらを使用している可能性があります。
例えばmap()、reduce()、filter()メソッドです。
あなたはすでに高階関数を書いたり使ったりしているかもしれませんが、その重要性に気付いていない方もそうでない方も
この記事の終わりまでにそれを解決します。JavaScriptのコールバック・高階関数の概念を理解していきましょう。
関数については下記で解説しております。
コールバックとは、別の関数が終了した後に実行される関数となります。
また学習の際に高階関数という用語に出くわした事があるかと思います。
コールバックと高階関数は、JavaScriptで最も誤解されている概念の一部であり、複雑に思えるかもしれませんが、そんな事はありません。
まずは何かを学び始める前に、それらがなぜ重要なのかを理解する必要があります。
重要性
高階関数を使用すると、シンプルでクリーンなコードを記述できます。
この構成は、単純で読みやすいコードになりテストとデバッグが簡単となり、バグが少なくなります。
コードを簡潔にしたい場合、コードをより柔軟で保守しやすくするために特に役立ちます。
コールバック関数
コールバック関数はJavaScriptの重要な部分の1つです。
これは主に、同期タスクまたは非同期タスクの完了に応答して関数を実行するために使用されます
ほとんどが非同期動作で非常に役立ちます。
例えば、ユーザーがボタンをクリックするタイミングは誰にも分かりません。
なので、ボタンのクリックイベントにコールバック関数を渡し、ユーザーがボタンクリックするたびにコールバック関数が実行され、それに応答されます。
Webサイトをインタラクティブに設計し、反応できるようになります。
高階関数
ご存知かもしれませんが、JavaScriptでは関数はオブジェクトです。
そのためJavaScriptの高階関数は、関数を引数として受け入れたり、関数を返し他の関数を操作する関数を高階関数と呼び、引数として渡す関数をコールバック関数と呼びます。
高階関数は1つ以上の関数を引数として受け取り、または関数の結果を返す関数となります。
関数が2番目の関数(名前付きまたは無名)を引数として受け入れる場合となります。
function higherOrderFunc(greeting) { greeting(); } higherOrderFunc(function(){ console.log('Hello'); //Hello }); //アロー関数の場合 higherOrderFunc( () => console.log('Hello') );
上記では、関数の呼び出しの引数に他の関数を受け取っています。
結果として別の関数を返しています。
また下記は返された関数を二重括弧を使用して呼び出しも可能です。
別の関数から関数を返します。
function greeting() { return function() { console.log("Hello!"); } } greeting()(); //二重括弧
上記ではreturnで他の関数を返し、それを受け取っています。
引数の受け入れか、関数の戻りの両方を実行することは、必ずしも必須ではないことに注意してください。
いずれかを実行した際、その関数は高階関数になります。
冒頭で知らないうちに、高階関数を使用していた場合で見て見ましょう。
高階関数の一般的なメソッドはmap()、reduce()、filter()になりますがmap()メソッドで解説致します。
数字の配列を用意します。
function myArray() { const arr1 = [1, 2, 3, 4] const arr2 = arr1.map(function(num) { return num * 2 + ' ' }) return arr2 } console.log(myArray()) // 2, 4, 6, 8
上記は単純な掛け算(乗算)をしています。
最初の配列の2倍の値を含む、新しい配列を作成しそれを返します。
mapは引数として、他の関数を受け入れています。
const arr2 = arr1.map(function(num) { return num * 2 + ' ' })
他の関数で操作をしています、この時点でこれは高階関数となります。
別の例でも見てましょう。
const arr = [1, 2, 3]; const setArray = arr.map(function(x) { return x + 1; }); console.log(setArray);// 2, 3, 4 //アロー関数 const arr = [1, 2, 3]; const setArray = arr.map( (x) => { return x + 1; }); console.log(setArray); // 2, 3, 4
上記では単純な足し算(加算)をしており、map関数は引数で他の関数を受け入れています。
このように、特に初学者は便利だからmapメソッドを使用していたが、これが高階関数だとは知らずに使用していた、という事があります。
定義上、高階関数は関数を受け取るか、または返す関数にすぎません。
それだけであり、これ以上深く考える必要はありません。
コールバック関数
コールバック関数の場合は、外部関数内で呼び出される引数として別の関数に渡される関数でもあります。
コールバック関数は後で実行される関数であり、また高階関数とは違い値、関数、または何も返さない可能性があります。
そして高階関数の実行中に呼びだします。
つまり高階関数の引数に渡された関数を呼び出します
別の関数に引数として渡す関数は、コールバック関数と呼ばれます。
そして抑えておくべき点がいくつかあります。
コールバック関数を作成するには、無名関数、アロー関数、または引数として渡すの3つの方法があります。
• 名前付き関数をコールバックとして使用する
• コールバックとして無名関数を使用する
• 同期コールバックおよび非同期コールバック
• 別のコールバック内でコールバックを呼び出す
まずは引数に名前付き関数を渡すコールバックを見ていきましょう。
function callback() { return "Hello, "; } //高階関数 function greeting(Message, name) { console.log(Message() + name); } greeting(callback, "Tarou"); //実行時の引数に関数と値を渡す // 出力 Hello, Tarou
引数として渡されているのが分かるかと思います、関数と値(文字列)です。
つまり高階関数に渡すための関数はコールバックという事になります。
下記も別の例となります。
// 通常の関数 function myFunction() { console.log("コールバックが呼ばれた"); } //高階関数 function otherFunction(callbackFn) { callbackFn(); } otherFunction(myFunction); //関数を渡す //出力 コールバックが呼ばれた
さらに別の例でも見てみましょう。
// 高階関数 function greet(name, callback) { console.log('Hi' + ' ' + name); callback(); } function callMe() { console.log('I am callback function'); } //実行時に他の関数を渡す greet('Taro', callMe); //出力 Hi Taro // 出力I am callback function
関数の呼び出し中のgreet()に2つの引数が渡されています。
greet('Taro', callMe);
上記でしたら文字列の値と関数になります。
『callMe』関数はコールバック関数となります。
別の関数内で引数として渡される関数は、コールバック関数となる。
コールバックとして無名関数を使用する
コールバック関数は無名関数にする事が可能です。
コールバック関数と無名関数は基本的にはArray.prototype.map()などの他の関数に渡して非同期コールバックとして使用される事が多いです。
下記はmapを使用しないコールバックの無名関数の場合になります。
let greeting = function (name){ console.log("Hello" + " " + name) } greeting("Tarou") //Hello Tarou // アロー関数 const arrowFunc = (name) => `Hello${name}` console.log(arrowFunc("Tarou")) // Hello Tarou
アロー関数なら、より短い関数構文を記述できるようになります。
function setGreeting(name, x) { x(`Hello ${name}!`); } setGreeting('Tarou', (greeting) => { console.log(greeting); });
アロー関数は名前付きで関数を使用する事はできません、変数扱いとなるなので無名関数となります。
無名関数ではデバッグが難しくなったりします、なので無名関数の場合はなるべく慎重に扱っていくように心がけて下さい。
コールバックはあらゆる場所で使用されます。
window.addEventListener("click", callback => { 何かしらの処理 })
DOMイベントだけでなく、一般的なのは例えばタイマーなどでも活用したりします。
setTimeout ( () => { } 、3000); // 3秒後に実行
ではaddEventListenerにる、より一般的な例を試してみましょう。
DOMイベントリスナーでのパターンは、コールバックを説明するときに使用される典型的な例となります。
単純なhtmlボタンと、htmlで参照されるindex.jsファイルを作成します。
<body> <button id="myButton"></button> </body>
let myButton = document.getElementById('myButton') myButton.addEventListener('click', response); function response() { console.log('クリックされました。') }
まず、responseは変数としてaddEventListenerに渡されます。
クリックが発生したら、responseコールバック関数が呼び出されることを信頼します。
呼び出される様子は見えませんが、実際に呼び出されます、それはconsole.log('クリックされました。') が実行されることで、その結果を見ることができます。
つまり冒頭で解説した通り、ユーザーがボタンをクリックするタイミングは誰にも分かりません。
ですので、ボタンのクリックイベントにコールバック関数を渡し、ユーザーがボタンクリックするたびにresponseコールバック関数が実行され、それに応答し『クリックされました。』と表示を確認できます。
ボタンが押されていない場合、呼び出される事はありません。
addEventListener の引数に渡されたresponseが渡された瞬間、それはコールバック関数になります。
ボタンを押す事により、ユーザーの要求に応えます。
今ではなく、後で呼び出しいてる事になります、この機能のおかげで、Webサイトをインタラクティブに設計し反応する事ができるということです。
コールバックは、単に渡す関数定義です。
この概念を理解することは、Vanilla.JSのイベントハンドラからReactでのStateおよび状態管理や、すべてを理解する上で非常に重要なので、注意してください。
コールバックには、同期コールバックと非同期コールバックの2種類があります。
同期コールバック
JavaScriptはデフォルトで同期言語であり、シングルスレッドです。
これは、コードが新しいスレッドを作成して並行して実行できないことになります。
つまり、コードが上から下に順番に実行される場合はそれは同期的です。
それが本来のプログラム動作です。
同期コールバックは、コールバックを使用する高階関数の実行中に実行されます。
高階関数の実行中に実行されると、高階関数はコールバックの実行が完了するまで実行を完了しません。
つまりコールバックは高階関数を一時的にブロックします。
前述で学んできた、コールバックは全て同期コールバックです。
function func1(test) { console.log('呼び出し 1'); test(); } func1(function func2 () { console.log('完了'); }); console.log('呼び出し 2');
上記の同期では、func1はコールバックと同期され実行されます、そしてfunc1が戻るまで実行はブロックされています。
先に進む前にコールバックが実行されるようにします。
高階関数と同時に実行されるため、func2は同期コールバックとなります。
非同期コールバック
非同期コールバックは、コールバックを使用する高階関数の実行後に実行されます。
非同期はJavaScriptが操作の完了を待機する必要がある場合は待機中に完了するのを待たずにコードの次の行の実行を開始することです。
つまりJavaScriptが操作の完了を待機する必要がある場合、待機中に残りのコードを実行することになります。
つまり同期コールバックとの違いは、同期コールバックはすぐに実行されるのに対して、非同期コールバック関数は実行を延期される事です。
前述で紹介致しました、setTimeout()に精通している場合は、確認ができます。
JavaScriptには、『setTimeout』と呼ばれる組み込みの関数があります。
このメソッドは関数を呼び出すか、指定された期間(ミリ秒単位)後に式を評価します。
function setNumbar(){ console.log(1); setTimeout(function() { console.log(3); }, 3000); console.log(2) } setNumbar(); /* 1 2 3 * /
上記の関数は、何かが発生した後に3秒が経過した後に呼び出されていますが、コールバックはsetTimeoutへの引数として渡される関数です。
もし同期的であれば、本来は上から下に処理されていくので下記のように出力されます。
/* 1 3 2 * /
JavaScriptは上から下に実行されますが、常に順番に実行されるとは限りません。
下記は別の例となります。
function greet() { alert(`Hello`) } function myName(name) { alert(` My name is.${name}!`) } setTimeout(greet, 2000) myName('Tarou')
greet関数を呼び出し2秒後に実行します。
その間に次に進み、すぐに『My name is.Tarou』とアラートされます。
その後2秒経過しgreet関数が実行され、『Hello』とアラートされます。
つまり、greet関数が先に呼び出されても逆に呼び出されます。
しかし、これはすべてのコールバック関数が非同期であることを意味するわけではありません。
これらは同期的に使用することも可能です。
無名関数での非同期コールバック
もちろん必要に応じて、非同期でもJavaScriptの新しい関数であるES6アロー関数でコールバック関数を作成することもできます。
function setNumbar(){ console.log(1); setTimeout( () => { console.log(3); }, 300); console.log(2) } setNumbar() // 別の例 setTimeout( ()=> { console.log("このメッセージは3秒後に表示されます") }、3000)
let Arr = [1,2,3,4] function myCallback(x) { return + 1 } console.log(Arr.map( function(x) { //無名関数 return myCallback(x + 2) })); // [1, 1, 1, 1]
コールバック側の関数には名前を付けます。
コールバック関数は別の関数内で呼び出されたときに引数を取りません、しかし無名関数は引数を取り入れることができます。
非同期コールバックはネットワークの速度や画像のサイズによっては、リモートサーバーから画像をダウンロードするのに時間がかかる場合があります。
setTimeoutで画像のダウンロードが完了するまでの待機時間を操作してみて下さい。
コールバックの注意点
非同期コールバックが管理不能になるまで複数に深くネストされたコールバックを回避することを強くお勧めします。
JavaScriptで深くネストされたコールバックは、一般的に『コールバック地獄』と呼ばれており、コードの可読性(読みづらい)、他のスコープの問題を引き起こす可能性があります。
//コールバック地獄の例 const messageHell = [] function hell() { setTimeout(function() { messageHell.push('Hello'); setTimeout(function() { messageHell.push('world'); setTimeout(function() { messageHell.push('javascript'); setTimeout(function() { messageHell.push('Please do not give me callback hell.'); console.log(messageHell.toString()) },100); },100); },100); },100); } hell(); // 出力 Hello, world, javascript、Please do not give me callback hell.
上記はあくまでも例ですが、さらに複雑な処理になると…想像してみて下さい。
これはどのプログラミング言語でも発生する可能性があり、非同期操作でより一般的な事でもあります。
ですが回避方法はございます。
コールバック地獄を回避する為の1ステップへ進みましょう。
下記で学ぶ事ができます。
Promiseを既に精通している場合はasync/awaitへ進んで下さい。
まとめ
コールバックは、後で実行される引数として別の関数に渡される関数であり、無名関数、アロー関数を使用できます。
高階関数は、他の関数を引数として受け入れる関数となります。
そしてコールバック関数は、同期または非同期にすることができます。
コールバック関数と高階関数の違い、およびいつ使用するのかまた、それらの重要性が解決される事を願っています。
本日は以上となります。
最後までこの記事を読んで頂きありがとうございます。