JavaScriptのクロージャって何?を解決する【具体的な使用例付き】
目次
今回はJavaScriptのクロージャについて解説します。クロージャとは、一言でいうと、関数とレキシカル関数の組み合わせです。…と言われても、ちんぷんかんぷんですよね。このブログを読み終えた頃には理解できるようになっているので安心してください。また、クロージャの具体的な使用方法も合わせて解説します。
- JavaScriptのクロージャの理解に苦しんでいる
- フロントエンドエンジニアを目指している
- クロージャは分かったけど使い方がわからない
そもそもクロージャとは何か?
クロージャとは関数とその関数が定義されたレキシカル関数の組み合わせである。
MDN
MDNによると、クロージャとは、関数とその関数が定義されたレキシカル関数の組み合わせだそうです。
…とてもわかりにくいですよね。そもそも、レキシカルってなんやね〜ん!!と言う人も多いかと思います。
そこも含めて解説するので安心してください。
クロージャを理解するためのポイント4選
クロージャを理解するために知って置かなければいけないJavaScriptの仕様が4つあります。
- JavaScriptは関数の中に関数を定義できる
- JavaScriptは関数を変数として持ち運べる(JavaScriptの関数は第1級関数)
- JavaScriptの関数は親の関数の変数を使える
- JavaScriptの関数は親の関数の変数を使える
これら4つのポイントについて詳しく解説していきます。
1. JavaScriptは関数の中に関数を定義できる
JavaScriptは関数の中に関数を定義できます。
あれ?そんなの当たり前じゃないか!と思った方もいるかと思います。実は、全てのプログラミング言語で関数の中に関数を定義できるわけではないのです。
実際にC言語やJavaという言語は原則関数の中に関数を定義することはできません。
function parent() {
function child() {
console.log('私は子供です。');
}
return child;
}
実際にコードで見てみると上記のようなコードになります。関数の中に関数が定義されているのが見て取れると思います。
2. JavaScriptは関数を変数として持ち運べる(JavaScriptの関数は第1級関数)
JavaScriptでは関数もオブジェクトです。つまり、関数も他のオブジェクト同様に、変数や配列、もしくはオブジェクトに「値」として格納できます。また、関数を引数として関数に渡したり(コールバック関数)、関数を関数の戻り地にすることもできます。これらの特徴を持っていることから、JavaScriptの関数は第一級関数です。
関数が第一級関数という仕様も、関数の中に関数を定義できる仕様と同じく、全てのプログラミング言語でサポートされているわけではありません。
では実際に関数を変数として持ち運べるかどうか、コード上で確認します。
function hello() { function child() { console.log('私、helloの子供だよ!'); } return child; }
function receive(v) { console.log('引数で受け取ったvを実行するよ!'); v(); }
// hello関数の返り値をxに代入 const x = hello();
console.log(x);//結果:child オブジェクト(関数)
x(); //結果:私、helloの子供だよ!
receive(x); //結果:私、helloの子供だよ!
少し処理の流れを追うのが難しいかと思いますが、少し説明すると、hello関数はchild関数を返り値として返す関数です。その証拠にconst x = hello();
でxに代入されたhello関数の返り値をconsole.log(x);
すると、child オブジェクト(関数)がコンソールに表示されます。
また、receive関数は引数vで受け取った関数を実行する関数になります。receive(x);
では引数にx、つまりchild オブジェクト(関数)を受け取り、実行しています。
JavaScriptでは、上記の例の、const x = hello();
のように関数を変数に格納できたり、receive(x)
のように、関数を引数として関数に渡したり、hello関数のように、関数を関数の戻り地にすることもできる。そのような関数のことを第一級関数と呼びます。
3. JavaScriptの関数は親の関数の変数を使える
通常関数が2つあった場合、一方の関数内から他方の関数の変数や定数にアクセスすることはできません。ただし、関数が親子関係にある場合のみ、子関数から親関数の変数や定数にアクセスすることができます。この仕様のことをスコープチェーンと呼びます。
コードで見ていきましょう。
この例は、関数が2つあった場合、一方の関数内から他方の関数の変数や定数にアクセスすることはできないことを表すコードです。foo関数からhoge関数内の定数にアクセスしようとしていますが、undefined
が返されています。
function hoge() { const x = 1; }
function foo() { console.log(x); }
foo(); //結果:undefined
次は、親の関数から子の関数の変数や定数にアクセスできないことをコードで確認していきましょう。コード上では、oya関数からko関数内の定数yにアクセスしようとしていますが、undefined
が返されています。
function oya() { const x = 1; function ko() { const y = 2; }();
// 親から子供の関数内の定数にはアクセスできない console.log(y); //結果:undefined
}();
最後に、関数が親子関係にある場合のみ、子関数から親関数の変数や定数にアクセスすることができることをコードで確認します。
function oya() { const x = 1; function ko() { const y = 2;
function mago() { const z = 3; // 子供から親の関数内の定数にはアクセス可能 console.log(x); //結果:1 console.log(y); //結果:2 console.log(z); //結果:3 }(); }();
}();
mago関数からko関数で定義された定数yと、oya関数で定義された定数xにアクセスしていますが、問題なく動作していることが確認できました。
このように、JavaScriptでは、関数が親子関係である場合、子関数から親関数へのアクセスのも許可されています。
4. JavaScriptの関数の親とは産みの親のことである
このポイントが最もクロージャと関係してくるところなのですが、プログラミングの世界において、スコープは2種類存在します。それは、ダイナミックスコープとレキシカルスコープです。
このままだと理解しづらいと思うので、ダイナミックスコープとレキシカルスコープを動物の刷り込み現象に例えて説明します。刷り込み現象とは、動物が生まれてはじめて見たものを、実際に親であるかどうかに関係なく、親だと認識する現象のことです。
まず、関数を定義している状態を、卵の状態であると仮定します。そして、関数を実行した時を、卵から産まれる状態であると仮定します。
このとき、卵から孵って最初に見たものを親とすることが、関数を実行する時の親関数を親とするため、ダイナミックスコープに当たります。また、卵を産んだ親(実親)を親とすることは、関数定義時の親関数を親とするため、レキシカルスコープに当たります。
どうですか?理解できましたでしょうか?
そして、JavaScriptではスコープの仕様として、レキシカルスコープを採用しています。つまり、関数定義時の親関数を親とするということです。先程の刷り込み現象の例えでは、卵を産んだ親(実親)を親とするということです。
実際にコードで確認しましょう。
function oya() { const x = 1; function ko() { const y = 2; console.log(x);//結果:1 console.log(y);//結果:2 } return ko; }
function kyodai(v) { const x = 3;
v();
}
// oya関数の返り値であるko関数を代入 const oya_return = oya();
// 定義された時点の親関数のスコープを参照する→レキシカルスコープ kyodai(oya_return); //結果: x:1 y:2
この例では、kyodai(oya_return);
により、kyodai関数が実行されました。kyodai関数は引数に渡された関数を実行するので、引数に渡されたoya_rerurn
つまり、ko関数を実行します。
JavaScriptではレキシカルスコープを採用しているため、ko関数内での定数xは、ko関数定義時の親関数であるoya関数のconst x = 1
を参照します。JavaScriptがダイナミックスコープを採用していた場合は、ko関数内での定数xは、kyodai関数内のconst x = 3
を参照しているでしょう。
つまり、JavaScriptでは関数の親とは産みの親のことだということです。
JavaScriptのクロージャについてのまとめ
ここまで説明したことをまとめます。
冒頭に説明した、”クロージャとは、関数とその関数が定義されたレキシカル関数の組み合わせである”というのは、"クロージャとは、関数と産みの親の関数(と祖先全員)が持っている変数を合わせたもののこと"だということです。また、クロージャの親関数のことをエンクロージャと呼びます。
クロージャはどうやって使うの?
ここまでクロージャについて説明してきた中で、「では、クロージャっていつ・どうやって使うの?」「どんなメリットが有るの?」と思った方も多いと思います。ここからはクロージャの具体的な使用方法について説明します。
1番の使い所はコールバック関数
クロージャの一番の使い所はコールバック関数です。フロントエンドの処理は、ユーザの入力に対して、応答する形で行われる事が多いです。addEventListener
を使用した処理は代表的な例ですよね。このような、ユーザの入力に対して、応答する形で行われるアプリケーションのことをイベント駆動のアプリケーションと呼びます。
こういったイベント駆動の処理にはコールバック関数が使用されます。そういった場面でクロージャが活躍します。
イベント駆動でない例としては、プログラムを起動すると自動で処理を続けて、終わったら終了するといったプログラムです。例えば、起動したら掃除してくれるプログラムのような感じです。
関数のシグネチャ
コールバック関数でのクロージャの使用方法を説明する前に、関数のシグネチャについて知っておかなければいけません。
関数のシグネチャとは、関数の引数の数と型及び、戻り値のことです。イベントに対して登録するコールバック関数は、予めブラウザによってシグネチャが決められています。決められたシグネチャの関数をコールバック関数として引数に登録すると、イベント発生時にコールバック関数を呼んでくれる仕様になっています。
それでは、コード上で確認していきます。ここでは、「もし14時前に帰宅した場合は、銀行にお金を振り込みに行くように自分に伝言する。18時以降に帰宅した場合はご飯を作る。」というプログラムを例に取ります。
addEventLister('帰宅', (${時間}) => { if(${時間}が14時前だったら) { 銀行にお金を振り込みに行くように私に伝言する。 } else if(${時間}が18時以降なら) { ご飯を作る。 } });
// 実際のJavaScriptでは${時間}はeというイベントオブジェクトに渡される。
このコード例では、addEventLister
に決められたシグネチャとして”時間”が設定されています。そのため、時間に応じて命令を変更することはできるますが、時間以外の条件で処理を分けたい場合にはそれができません。これが関数のシグネチャです。
クロージャで関数をカスタマイズする
関数のシグネチャのところで説明したとおり、関数の引数にはブラウザによって決められたシグネチャ歯科渡すことはできません。上記の例だと決められたシグネチャとは時間でした。では、上記のプログラムを、「日曜日の場合は何もしなくていい」という処理にしたい時にはどうしたらいいのでしょうか?
こういったときにクロージャが活躍します。実際にコード上で見ていきましょう。
function createCallBackFunction(${曜日}) { return function(${時間}) { //レキシカル環境として${曜日}を持っている。 if(${曜日}が日曜日でなければ) { if(${時間}が14時前だったら) { 銀行にお金を振り込みに行くように私に伝言する。 } else if(${時間}が18時以降なら) { ご飯を作る。 } } } }
addEventLister('帰宅', createCallBackFunction(今日の曜日));
createCallBackFunction();
は引数に${時間}をとる関数を返す関数です。そのため、addEventLister
のコールバック関数になっていても問題はありません。
このように、クロージャを使用することで、関数の中に変数を増やすことができます。そのため、実質的に決められたシグネチャ以外の引数を関数に渡すことができるようになります。
まとめ
- 関数と産みの親の関数(と祖先全員)が持っている変数を合わせてクロージャと呼ぶ
- JavaScriptが採用しているレキシカルスコープとは、レキシカルスコープとは、関数定義時の親関数を親とするスコープである
- クロージャを使うことで決められたシグネチャ以外の引数を関数に渡すことができる
- クロージャを使うことでネストが深くなることを防ぐことができる
いかがでしたでしょうか?少しでもクロージャのことが理解できたら幸いです。そして、少しでも理解できたと思った方は実際にコード上でクロージャを使用してみましょう。アウトプットすることで更に理解が深まると思います。