【JavaScript】 クロージャとは?要点をまとめてみる
更新日:2021/01/27
JavaScriptにはクロージャーという概念がある。
20年前のC言語プログラマな僕が、気が狂いそうになった原因である。
僕は最近になって、ようやくわかってきた気がするので、忘れないうちに要点をまとめてみる。
クロージャとは
プログラム言語の概念的な定義としては、関数が定義された時点での環境を保存しておき、関数の実行時に再利用できる構造を指します。
スコープ解決の仕組み
クロージャについて学ぶには、まずはJavaScriptのスコープ解決の仕組みを知る必要があります。
JavaScriptは関数やブロック({ }で囲まれた範囲)毎に、環境レコードというものを持っています。
この環境レコードには、その関数やブロックで使用されている変数および関数の名前と、それらの実体への参照、さらには、外側の環境レコードへの参照が保存されています。
環境レコード
- 変数の名前:実体への参照
- 関数の名前:実体への参照
- 外側の環境レコードへの参照
外側の環境レコードへの参照をたどっていくと、最終的に一番外側のグローバル環境レコードに行き着きます。
次に次のようなコードにおける、関数と環境レコードの関係を考えてみます。
const a = 10;
function func1(){
const b = function(){
console.log( a );
}
return b;
}
次の図のような関係になります。
一番内側の関数で変数aが参照されると、環境レコードを2つたどってa=10に行き着き、10が変数aの値として採用されます。
もし一番外側の環境レコードまで達してもaがみつからないときは、undefinedがaの値として採用されます。
このようにJavaScriptは環境レコードを外側に参照していくことで、スコープ解決がおこなわれています。
関数が定義された時点での環境を保存
次に上のコードの最後で、次のコードを実行してみます。
const c = func1();
変数cの値は、関数func1で返された関数になります。
このとき、付随する環境レコードもそのままセットされて返されています。
このような、関数と環境レコードの組み合わせをクロージャと呼びます。
ただし、func1も関数と環境レコードの組み合わせであるため、クロージャであると言えます。
しかしJavaScriptの解説書などでは、便宜的に関数外に変数として保存したクロージャを、『クロージャ』として説明しているケースが多いです。
理由は、JavaScriptの解説をするとき『クロージャの特徴を活かして○○する』という言葉が非常に便利なためです。
このときのクロージャは、関数外に変数として保存したクロージャを指しています。
いろいろと都合がいいのです。
クロージャの特徴
外部に取り出したクロージャで特に注意したいのは、外部への参照は何も加工されていないことです。
そのためクロージャがfunc1内の外側で実行されても、func1内で実行されたときと同じようにスコープ解決されます。
つまり、どこで実行しても変数aの値は同じになるということです。
エンクロージャの特徴
上のコードで関数fun1は、内部でクロージャを作成してから外部に返しています。
このようにクロージャを返すことを目的とした関数をエンクロージャと呼ぶことがあります。
クロージャの使い方
クロージャの特徴がわかったところで、次は具体的な使用方法をお伝えします。
プライベート変数の作成
次のような、関数内で定義された関数を返す関数があるとします。
function func( b ){
// ローカル変数c初期化
const c = b * 2;
// 内部関数初期化&リターン
return function( ){
console.log( c );
};
// 関数終了:ローカル変数c破棄されるはず
}
const a = func( 10 );
a( ); // func() から返された関数を実行
JavaScriptでは、ローカル変数は関数実行時に初期化され、関数終了時に破棄されます。
しかし、他の変数から参照されているときは、『まだ必要な変数』と判断されて削除されません。
上のコードは、funcから返されたクロージャを変数aに格納しています。
これにより、環境レコードが外部で参照され、それに連動して変数cが『まだ必要な変数』と判断され削除されません。
また変数aは、console.log( c )を実行するだけの関数です。
その外側で定義されている変数cを直接操作することはできません。
そのため、存在はしているけれど外部から参照や操作ができないプライベート変数になっています。
異なるプライベート変数の作成
もう少し詳しくお伝えします。
次の例を見てください。
let message = "さんこんにちは";
function a( name ){
let m = name + message;
return function ( ){ // クロージャ(1)
console.log( m );
};
}
let a2 = a( "Taro" );
let a3 = a( "Hanako" ); // 2回呼び出したので変数mが上書きされるはず
a2( ); // Taroさんこんにちは ← 上書きされていない!!
a3( ); // Hanakoさんこんにちは
異なる引数で関数aを実行して、クロージャ(1)を2回作成しています。
このとき関数aの引数nameと変数mは、クロージャ(1)のプライベート変数となります。
内部的には、次のようになっています。
message : "さんこんにちは";
a2 :
name : "Taro"
m : "Taroさんこんにちは"
function ( ) {
console.log( m );
};
a3 :
name : "Hanako"
m : "Hanakoさんこんにちは"
function ( ) {
console.log( m );
};
引数nameと変数mは、同じ関数から作成したクロージャ間であっても、共有されていません。
つまり同じコードから、異なるプライベート変数をもつ関数を簡単に作成できるのです。
もう少し具体的な例を挙げてみます。
function rateConversion( countryName , rate ){
const name = countryName;
const rateValue = rate;
return function( yen ){
return `${yen / rateValue} ${name}`;
};
}
const dollar = rateConversion( "米ドル" , "110" );
const euro = rateConversion( "ユーロ" , "123" );
console.log( "1000円は" + dollar( 1000 ) + "です");
console.log( "1000円は" + euro( 1000 ) + "です");
同じ関数で、if文を使わずに米ドルまたはユーロのレート計算をしています。
nameとrateValueはプライベートなので、後から変更できません。
つまり米ドル用の関数とユーロ用の関数を同じコードから作成したことになります。
やりがちな間違い
僕がクロージャの仕組みをあまり理解していなかった頃に、やってしまった間違いをお伝えします。
const data = 10;
function func1(){
console.log( data );
}
function getClosure(arg){
const data = arg;
const f = func1;
return f;
}
const closure = getClosure(100);
closure(); // 100と表示されるはず!!
上のコードは、getClosure関数に渡した引数を変数dataにセットして、関数fuc1を返しています。
返された関数はクロージャとして機能し、実行すると外部変数dataをコンソールへ出力します。
今回は100をdataとして渡しているので、closureを実行するとコンソールに100と表示されるはずです。
しかし、実際には10が表示されます。
どうしてでしょうか?
関数内での変数検索は、あくまでも定義されている位置を基準としておこないます。
定義位置とは、関数内のコードが記述されている場所です。
ここでは、function func1(){…}が記述されている位置です。
その位置から外部の変数dataを探すと、const data = 10;が見つかります。
その結果、10が表示されます。
では、100を表示したい場合、どのようにコードを記述すればいいのでしょうか?
答えは、次のようにgetClosure内で関数を定義します。
function getClosure(arg){
const data = arg;
return function (){
console.log( data );
};
}
他言語を学んだ人は、関数の中で関数を定義することに抵抗があることがあります。
そのため、正しいコードを書いた後、外部に関数を出したくなってきます。
すると、最初のようなコードになります。
しかしこの場合はNGなので、注意しましょう。
なお、最初のコードの次の部分は、定義しているっぽさを出すための演出です。
const f = func1; ← func1を定義しているっぽい
return f;
冗長なので、次のように記述するのが効率的です。
return func1;
おまけ:複数の関数を同時に返す
次の例のように複数の関数をクロージャとして返すことができます。
function a( name , familyname){
return {
getName:function ( ){ return name;},
getFamilyName:function ( ){ return familyname;},
getFullName:function ( ){ return name + ' ' + familyname;}
}
}
const b = a ( "Taro" , "Yamada");
console.log( b.getName() ); // Taro
console.log( b.getFamilyName() ); // Yamada
console.log( b.getFullName() ); // Taro Yamada
これを利用すると、隠蔽された変数にゲッター/セッターを定義することができます。
function a( name , familyname){
return {
set name ( v ){ name = v; },
get name ( ){ return name; }
};
}
const b = a ( "Taro" , "Yamada");
b.name = "Hanako";
console.log( b.name ); // Hanako
ゲッター/セッターについては、次のページを参考にしてみてください。
参考:【JavaScript】 ゲッター・セッターとは?必要性はあるのか?
余談:JavaScriptの仕様にクロージャはない
正直言うと、僕はJavaScriptのクロージャについてよくわかっていません。
それというのも、JavaScriptの仕様書にはクロージャについて何も書かれていないからです。
おそらくクロージャという一般的概念を念頭に置いて仕様設計をおこなっていると思いますが、何も書かれていないということは、JavaScriptにはクロージャの概念がないということです。
ということは、解説書などで説明しているクロージャはプログラミングにおける一般的な概念をJavaScriptに当てはめて説明していることになります。
しかし元々ないだから、うまく当てはまるわけがありません。
解説書などで説明しているクロージャは、レキシカル環境上でスコープを解決する仕組みの一部を取り出そうとしたものです。
一部を取り出しただけなので、同じようなものが他にも存在しています。
JavaScriptに詳しくないうちは素直に受け取っていられるのですが、仕組みを勉強していくと、「これもクロージャじゃないの?」と広がっていき、「仕組みそのものがクロージャでは?」「いやいや、そうではないかも?」「よくわからん」となります。
そもそもJavaScriptにないのに、どうしてクロージャという言葉を使っているのかも疑問です。
もしかしたら「JavaScriptでプライベート変数を作成する手法」を簡潔に表す言葉が欲しかったのかもしれません。
「それはクロージャを使うと簡単ですよ」( ー`дー´)キリッ
なんだか、できる人間に見えますね。
たぶん、これが正解です。(根拠はありません)
そもそもクロージャの一般的な概念がわからないです。Wikipedia(https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3)を見るといろいろ書いてあるけれど、言語実装からのフィードバックが含まれていて、概念としてのクロージャがどのようなものかの切り分けがよくわからない。現在のクロージャの概念は、もしかしたら『クロージャとはクロージャっぽい何か』ということかもしれない。
更新日:2021/01/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。