僕らは JavaScript を知らない - レキシカルスコープとクロージャとガベージコレクション Lexical Scope, Closure and Garbage Collection
参考:
- https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch2.md
- https://anond.hatelabo.jp/20070622101313
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Memory_Managements
- https://ja.wikipedia.org/wiki/%E3%82%AC%E3%83%99%E3%83%BC%E3%82%B8%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3
レキシカルスコープ
スコープ
そもそもスコープとは、変数や関数が参照できる範囲のことを言います。スコープの中で宣言した変数は同じスコープ内でのみ参照でき、スコープの外側から参照されることはありません。
例えば次のコードでは関数 foo
内で宣言された変数 b
は関数 foo
内では参照できますが、関数の外側で参照しようとすると ReferenceError
となります:
function foo(a) { var b = a * 2; console.log( b ); // 6 } foo( 3 ); console.log( b ) // ReferenceError: b is not defined
この関数によるスコープのことを関数スコープと呼びます。
スコープチェーン
次のコードでは変数 a
は関数内に宣言されてませんが、 ReferenceError
になることはありません。なぜなら、 foo
関数のスコープの外側のスコープに存在する変数 a
を使用するためです。現在のスコープに変数が存在しない場合、そのスコープをネストするより外側のスコープまで参照の範囲を広げていくこの仕組みを、スコープチェーンと呼びます。
function foo() { var b = a * 2; console.log( b ); } var a = 2; foo();
レキシカルスコープ
次のコードでは、 bar
の実行はグローバルスコープにて行われましたが、返ってくる値はグローバルスコープに存在する x
ではなく、 foo
による関数スコープ内の x
です。
このようにスコープが関数を実行した時ではなく、宣言したときに決定される特徴を持つスコープをレキシカルスコープと呼びます。
var x = 'global'; function foo(){ var x = 'local'; return function(){ return x } }; /* 実体は以下の通りで、グローバルスコープにおいて実行されるが、 返り値は `foo` 関数スコープの `x` function() { return x } */ var bar = foo(); x; // global bar(); // local
JavaScript はコードを実行する前に、そのコードを意味のある最小単位(トークン, Token) へと分解するレキシング (Lexising) を行います。 コード実行前のレキシング時に決定されるため、レキシカル (Lexical) スコープと呼びます。
上記のコードのように、スコープの階層が異なれば同じ名前を持つ変数を宣言することができます。これをシャドーイング (shadowing) と呼びます。 bar
は foo
スコープ内に変数 x
を見つけたため、これを使用し、グローバルスコープに存在する x
は無視されることになりました。グローバル変数 x
がローカル変数 x
の影に隠れた (シャドー, shadow) イメージです。
クロージャ
クロージャとは「関数が内包するスコープを保持する性質」のことを言い、クロージャ関数とはその性質をもつ関数のことを言います。
先程のコードも実はこのクロージャを利用していますが、いまいちわかりにくいので、もう少し分かりやすいコードを見てみましょう:
var counter = function(initialValue) { var count = initialValue; return function() { count++; return count; } } /* var count = 7; return function() { count++; return count; } */ var myCounter7 = counter(7); /* var count = 15; return function() { count++; return count; } */ var myCounter15 = counter(15); myCounter7(); // 8 myCounter7(); // 9 myCounter15(); // 16
上記の例では myCounter7
は、作成された時点での内部スコープを保持するクロージャ関数が代入されており、この中では count
の値 7
が取り込まれています。myCounter15()
では、count
の値 15
が保持されています。また、それぞれ関数実行後も count
の値が保持され続けていることも確認できます。
ではなぜ、レキシカルスコープを、そしてその内部の変数の値を関数実行後も保持することができるのでしょうか?それを知るためには、メモリの管理について理解する必要があります。
ガベージコレクション
変数や関数を宣言するとデータはメモリ領域を確保しますが、メモリ解放をしないとメモリリークと呼ばれる、ソフトウェアが使用できるメモリ領域が減っていく現象が発生します。
したがってメモリリークを防ぐためには適切なメモリ管理を行う必要があります。その方法は言語により異なりますが、 JavaScript ではガベージコレクションが採用されています。
ガベージコレクションでは、それを備えていない言語とは異なり、割り当てられたメモリが使用されてすでに必要なくなったときに自動的に開放されます。
どのようなときに、ガベージコレクションは割り当てられたメモリを「不要」と判断するのでしょうか。主にはその値が「参照」されているかどうかです。どこからも参照されなくなったら、それはガベージコレクションの対象となります。
具体例を見ましょう:
var obj = { x: 'foo' } obj = null;
オブジェクト { x: 'foo'}
は宣言時、変数 obj
から参照されています。したがって明らかにガベージコレクションの対象にはなりませんが、その後、 変数に null
を代入されたために、 { x: 'foo'}
を参照しているものはなくなりました。どこからも参照されなくなった時点で、このデータは不要と判断され、ガベージコレクションの対象となり、メモリ上から開放されます。
では、ここで先程のクロージャ関数の例を見てみましょう:
var counter = function(initialValue) { var count = initialValue; return function() { count++; return count; } } /* function(7) { var count = 7; return function() { count++; return count; } } */ var myCounter7 = counter(7); /* 実行している関数は以下の通りだが、`count` はグローバルスコープではなく、 関数の宣言時の外部スコープの変数 `count` を参照している var count = 7; return function() { count++; return count; } */ myCounter7(); // 8 /* もう一度実行したときも、 `count` の値は `8` のまま参照され続けているので返り値が `9` になる */ myCounter7(); // 9
このように、count
がガベージコレクションの対象とならずにデータを保持することができる理由は、関数が宣言されたときの変数 count
を参照するというレキシカルスコープの性質と、参照されている count
のデータはメモリ上から開放されず、データが保持され続けるというガベージコレクションの性質、これらふたつの性質を持っているから、ということができます。