【JavaScript】いまさらだけどクロージャを理解する
はじめに
JavaScriptにはクロージャというものがあります。恥ずかしながら、このクロージャについて理解が薄かったので、調べてみました。
クロージャとは
クロージャとは以下のようなコードで示されたものです。
function func(x) {
let val = x;
return function() {
val++;
console.log(val);
}
}
const f = func(10);
f();
f();
f();
この実行結果は以下のようになります。
11
12
13
f()
を実行するたびに関数func()
の内部変数val
の値がインクリメントされていきます。なぜこのようになるのでしょうか。
MDNのクロージャの項目には、以下のように書いてあります。
クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。
正直なところよくわかりません。「レキシカル環境」ってなんでしょう?
そこで、まず「レキシカル環境」から調べてみます。
レキシカル環境とは
レキシカル環境とは、関数呼び出しの際やスクリプト全体それぞれについて内部的に作成される隠れたオブジェクトのことです。レキシカル環境は2つのオブジェクトからなります。
- 環境レコード
- 外部のレキシカル環境への参照
環境レコードは、環境内で定義されたすべてのローカル変数をプロパティとして持ちます。たとえば、
let val;
let str;
val = 10;
str = "string";
function func(x) {
...
}
func(10);
上記のようなコードであれば、プログラム開始時のレキシカル環境は次のようになります。(イメージ。以下同じ)
const globalLexicalEnvironment = {
{
val: <uninitialized>,
str: <uninitialized>,
func(x) { ... }
}, {
outer: null
}
}
val
やstr
が<uninitialized>
(特別な内部状態です)なのは、開始時には解析エンジンはプロパティの存在を知っていますが初期化されていないためです。プログラムが実行され、val
やstr
が定義されると、まずundefined
となり、代入と同時に値がセットされます。
さらにプログラムが実行され、関数func()
が呼び出されると、新たなレキシカル環境が作成されます。
const funcLexicalEnvironment = {
{
x: 10
}, {
outer: globalLexicalEnvironment
}
}
このfuncLexicalEnvironment
は不要となった(どこからも参照されなくなった)時点でメモリから削除されます。
スコープチェーン
上記の例で関数func()
が以下のように定義されていたとします。
function func(x) {
let val = 1000;
console.log(x + val);
console.log(str);
}
この関数の実行結果は以下のようになります。
1010
string
変数val
は関数内で定義されていますが、str
は定義されていません。しかしJavaScriptでは問題なく出力できます。これについて、レキシカル環境を用いて考えてみます。
まず、関数実行開始時のレキシカル環境は以下のようになります。
const funcLexicalEnvironment = {
{
x: 10,
val: <uninitialized>
}, {
outer: globalLexicalEnvironment
}
}
funcLexicalEnvironment
は、プロパティとしてval
を持ちますが、str
は持ちません。val
はこのあと定義され代入されるので、値1000
がセットされます。
一方、str
は持っていないため、外部のレキシカル環境を参照します。参照される外部レキシカル環境(globalLexicalEnvironment
)はプロパティとしてstr
を持っており、この値が参照されます。このため、関数内でstr
が定義されていなくても値が出力できます。
このように、レキシカル環境を最近に作成されたものから順番にたどっていくことをスコープチェーンといいます。
関数が関数を返す場合
ここで、冒頭に示したクロージャとされるコードについてレキシカル環境を用いて考えてみます。
関数func()
実行終了時のレキシカル環境は以下のようになります。
const funcLexicalEnvironment = {
{
x: 10,
val: 10,
function() { ... }
}, {
outer: globalLexicalEnvironment
}
}
const globalLexicalEnvironment = {
{
function func(x) { ... },
f: funcLexicalEnvironment.function()
},
{
outer: null
}
}
このように、グローバルなレキシカル環境globalLexicalEnvironment
はプロパティf
の値として、関数func()
内で作成された無名関数への参照を持ちます。このとき、作成された無名関数はそれが作成されたレキシカル環境への参照[[Environment]]
を隠しプロパティとして持っています。
これにより、レキシカル環境funcLexicalEnvironment
はによりglobalLexicalEnvironment.f
を通して参照されるため、関数func()
の実行が終了してもメモリから削除されず、funcLexicalEnvironment.val
は生き続けます。そのため、f()
が呼び出されるたびにval
の値がインクリメントされていきます。
ここで改めて、MDNのクロージャの項目を見てみます。
クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。
まさに「(関数内で作成された)関数」と「関数が作成されたレキシカル環境」の組み合わせとなっています。冒頭で挙げた例がクロージャということになります。
参考
クロージャの使用例
クロージャの使い方として、変数や関数の隠蔽があります。ちょうどクラスにおけるプライベートメンバと同じようなものを実装する事ができます。MDNの例がわかりやすいので、これをそのまま持ってきます。
const makeCounter = function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value()); // 0.
counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.
counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
最初に定義される無名関数は、内部で変数privateCounter
と関数changeBy()
を持ちます。そして、戻り値として、関数increment()
, decrement()
, value()
を持つオブジェクトを返します。
定義された関数は、makeCounter()
によって呼び出され、変数counter1
とcounter2
にそれぞれ別のオブジェクトとして代入されます。
関数内で定義された変数privateCounter
と関数changeBy()
は、関数の外から直接アクセスできませんが、オブジェクトに定義された関数increment()
, decrement()
, value()
を通してアクセスできます。
makeCounter()
実行直後のレキシカル環境は以下のようになります。
const counter1LexicalEnvironment = {
{
privateCounter: 0,
changeBy(val) { ... },
{
increment() { ... },
decrement() { ... },
value() { ... }
}
}, {
outer: globalLexicalEnvironment
}
}
const counter2LexicalEnvironment = {
{
privateCounter: 0,
changeBy(val) { ... },
{
increment() { ... },
decrement() { ... },
value() { ... }
}
}, {
outer: globalLexicalEnvironment
}
}
const globalLexicalEnvironment = {
{
makeCounter: function() { ... }
counter1: counter1LexicalEnvironment.{
increment() { ... },
decrement() { ... },
value() { ... }
},
counter2: counter2LexicalEnvironment.{
increment() { ... },
decrement() { ... },
value() { ... }
}
}, {
outer: null
}
}
counter1
, counter2
それぞれで別のレキシカル環境が作成されることに注意します。各レキシカル環境へは、オブジェクトを通じて参照されます。このため、各レキシカル環境は関数の実行が終了しても存在し続け、プロパティprivateCounter
もメモリに残ります。
まとめ
JavaScriptにおけるクロージャについて調べてまとめてみました。「レキシカル環境」というものを考えることによって、今まで理解の薄かったクロージャの動作に対する理解を深めることができました。
クロージャを使うと、変数や関数を隠蔽することができ、保守性の高いプログラムが書けます。
ディスカッション
コメント一覧
まだ、コメントがありません