JSの基本をやり直すためにJavaScriptにとってのクロージャについて解説。
よくある解説として、
「クロージャとは関数閉包のことです。(終わり)」
のようなWiki情報だけで理解できる人は前提知識がいろいろありそうな人でないと無理な気がします。
そこで自分なりにピンとくるように噛み砕いて見ます(間違えていたり語弊があったら指摘をいただきたいです)
もう少し優しくクロージャって何?
JavaScriptにおけるクロージャとは2つの要素が組み合わさったものとして考えます。
①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと
②関数の中に関数があって返り値にもなっていること
①+②がクロージャです。スコープとはご存知だと思いますがここでは宣言された関数の範囲のことを指します。
そして①をよりJavaScript以外でも抽象化させた専門用語として"環境"と呼ぶそうです(レキシカル環境とも呼ぶ)。
さて話は戻して、ここでクロージャ(関数clojure)のソースコードを載せました。
const clojure = () => {
let member = 0;
const shareHouse = () => {
return (member += 1);
};
return shareHouse;
};
上のソースコードでいうところの
「①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと」は、
宣言した変数memberのことです。(プロパティに置き換えると { member: 0 } )
「②関数の中に関数があって返り値にもなっていること」は、
関数shareHouseにあたります。
クロージャの使い方
先のコードだけ見てもどのように使うのかがわかりません。以下の手順で実行すると見えてきます。
①返り値が return shareHouse となっているのでそのまま 関数clojure をそのまま実行して 返り値(関数shareHouse) を取り出す。
②shareHouseをそのまま2回実行する
するとどうでしょうか。
shareHouseから見て外側に宣言されたmemberの値が更新されています。
const clojure = () => {
let member = 0;
const shareHouse = () => {
return (member += 1);
};
return shareHouse;
};
const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2
当然ながら 変数member は 関数clojure の中でスコープされているのでよりグローバルな変数から代入されても値は保持されています。
//省略
const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2
let member = 10;
console.log(invite()); // 返り値 3
クロージャの使い道
「これの何が嬉しいの?」とここで疑問に思います。
①関数内にスコープされたプロパティ(キーと値)のまとまったデータをもつこと
②関数の中に関数があって返り値にもなっていること
と先に述べましたが、実際のコードにおいて②の関数とは計算だったり、APIを引っ張ってきたり、繰り返し処理をいれたりと機能が実装されています。
つまり、関数の処理結果を関数の範囲内に閉じ込めておきながら値を引っ張っていきたいケースで使うことができます。
省略したクロージャの書き方
しかし同じような手法はクラスにすれば同じことができます。(もしくはオブジェクト指向型の書き方で)
加えて、まだ直感的に理解しやすいのはクラスです。
しかしクロージャであればクラスよりもスッキリして書くことができます。
const clojure = (member = 0) => () => member += 1;
const invite = clojure();
console.log(invite()); // 返り値 1
console.log(invite()); // 返り値 2
clojureの引数に member = 0 をおくことで変数の宣言ができます。
そして 関数clojure の返り値である () => member += 1; を 関数clojure外 から実行すれば先の例と同じように値を保持したまま値を変更していくことができました。
もしくは、
const clojure = (member = 0) => (invited = 1) => member += invited;
const invite = clojure();
console.log(invite());
console.log(invite());
関数clojure の返り値にも引数を設定した形である (invited = 1) => member += 1; のような形で値を保持したまま値を変更していくこともできます。
ただし、返り値は一つしか取れないため複数の機能をもったものを実装したいのであればクラスの方が良いでしょう。
なんだかパッとしないなという人は下のコードを見れば理解しやすいはずです。
const clojure = (member = 0) => {
// const shareHouse = (invited = 1) => {
// return (member += invited);
// };
return (invited = 1) => {
return (member += invited);
};
};
// const clojure = (member = 0) => (invited = 1) => member += invited;
const invite = clojure();
console.log(invite());
console.log(invite());
改めてクロージャとは
ここまでくれば堅苦しい言い方、「クロージャとは関数閉包のことです。(終わり)」と言われても少しは言わんとすることがわかってきたのではないでしょうか。
更にここで、MDN Web Docsのクロージャの定義を引用します。
「クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。」
つまり、データ(プロパティのまとまり)の値の変化を関数の中の関数がコントロールしているといったところでしょうか。
ちなみにレキシカル環境とは何か、についてですが、レキシカル環境とは、静的スコープにおける関数の環境のことで、静的スコープとはJavaScriptのスコープの決まり方です。対照的に動的スコープがあります。
この点を掘り下げた内容に関してはJavaScript の原理:クロージャの真実が詳しいです。
なぜJavaScriptの関数を使ったクロージャは値を保持できるのか
const clojure = () => {
let member = 0;
const shareHouse = () => {
return (member += 1);
};
return shareHouse;
};
ここでの 変数member は 関数shareHouse の引数でもなければローカル変数でもありません(つまり自由変数)。しかし 関数shareHouse の中で値を参照できています。
これはJavaScriptのメモリ管理の仕組み上、クロージャにおける自由変数はメモリリークされずに残っているためです。
Reactの関数コンポーネントにおいてもクロージャの仕組みが用いられることがあります。