紹介とこのブログを書く理由
クロージャとthis の概念は、ずっと私を混乱させてきました。今でも、これたを正確に理解するのに時々苦労することがあります。そこで、これらを本当に理解するためには実際に練習するのが一番だと思いました。もし、私と同じようにこれらの概念に悩んでいる方がいれば、このブログが私たちの理解を深める手助けになればと思います。
DebounceとCurryを一歩ずつ実装しながら、これらの概念を深く掘り下げていきます。その過程で、クリージャが変数をどのようにキャプチャするか、そしてapply/call/bindが関数の呼び出しをどのように制御するかを見ていきます。
クロージャの理解
クロージャとは何か
クロージャはJavaScriptの中でも難しい概念の一つです。その概念をできる限り簡単に説明します。クロージャについて詳しく知りたい方は、優れた記事がたくさんありますので、それらを読むことをお勧めします。
クロージャは、生成されたの環境を「記憶」する関数です。つまり、外部の関数が実行を完了した後でも、内部の関数は外部の関数のスコープの変数にアクセスできるのことです。
クロージャの実行
例を見て、よりわかりやすくしましょう。
function outer() {
let count = 0
function inner () {
count++
console.log(count)
}
return inner
}
let innerFunc = outer()
innerFunc() // 1
この例では、inner関数がouter関数のスコープからcount変数をキャプチャしているため、innerはクロージャとなります。outer関数が実行を完了した後でも、innerはcount変数にアクセスすることができます。
一歩ずつクロージャを使用してDebounceの実現
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
const user = {
name: 'Y',
printMyInfo: function (age) {
console.log(`Hello! My name is ${this.name}. I'm ${age} years old.`)
}
}
const debouncedNamePrint = debounce(user.printMyInfo.bing(user), 3000)
debouncedNamePrint(30) // Hello! My name is Y. I'm 30 years old.
それでは、細かく説明しましょう。
まず、setTimeoutを追跡するために、timeoutIdという変数を定義します。この関数が遅延期間が終了する前に再度呼び出された場合、clearTimeout(timeoutId)が前のタイマーをキャンセルします。
次に、Debounceは関数を返す必要があります。この新しい関数は、既存のタイムアウトをクリアして遅延をリセットし、指定された期間の後にfuncを呼び出すための新しいタイムアウトを設定します。ここでクロージャが関係してきます。返された関数はtimeoutIdにアクセスし続けることができます。
カリー化の理解
カリー化は、関数を一連の関数に変換し、各関数が一度に1つの引数を取るようにします。
例
function currySum(a) {
return function(b) {
return function(c) {
return a + b + c
}
}
}
この例から、カリー化とクロージャがどのように連携して動作するかがわかります。一連の間すで返された各関数は、それぞれが受け取った引数にアクセスすることができます。
カリー化の実現
カリー化の基礎を理解して、シンプルなカリー化関数の実現方法を見てみましょう。
// implement
function curry(fn) {
return function curried(...args) {
if(args.length >= fn.length) {
return fn.call(this, ...args)
}
return curried.bind(this, ...args)
}
}
// example
function multiply(a, b, c) {
console.log(a * b * c)
}
const curriedMuti = curry(multiply)
curriedMuti(2)(3)(4) // 24
まず、fn関数を引数に取るcurry関数を定義します。関数の内部では、新しい関数curriedを定義して返します。この関数は引数を集めてfn関数に渡します。
次は、引数をチェックします。
-
fn.length:fn関数が期待する引数の数を示します。 -
args.length: 現在までに集めた引数の総数を示します。
チェックポイント: - もし
fn.lengthがargs.length以上であれば、必要な引数が揃ったことを意味します。その場合、fnをargsとともに呼び出し結果を返します。 - まだ引数が足りない場合、さらに引数を集めるための別の関数を消します。この新しい関数は、これまでに集めた引数と新しい引数を組み合わせて、更新された引数のリストで
curriedを再び呼び出します。
apply, call と bindの適用
上記の例では、関数が実行されるコンテキスト(this)を制御するために、apply、call、bindを使用しました。以下にそれらの違いを簡単に説明します。
// apply
func.apply(thisArg, [argsArray])
// call
func.call(thisArg, arg1, arg2, arg3, ...)
// bind
const boundFunc = func.bind(thisArg, arg1, arg2, ...)
これで違いがわかりやすくになると思います。共通点として、最初の引数は常にthisArgです。違いは以下の通りです。
-
apply: 引数は配列として提供します。 -
call: 引数は個別に提供します。 -
bind: 引数は個別に提供して、新しい関数を返します。
これで説明は以上です。ハッピーコーディング!