- クロージャによる開発の単純化
- 実行コンテキストでJavaScriptプログラムの実行を追跡する
- レキシカル環境で変数スコープを追跡する
- 変数の型を理解する
- クロージャの動作を調べる
クロージャを理解する
JavaScriptを書く上で、クロージャは欠かせないものである。クロージャを使うと、クロージャを定義した時点において、同一スコープに属するクロージャの外部の変数を操作することができる。
最も基本的なクロージャは以下のようになる。下記サンプルで、outerFunction関数は、outerValueにアクセスできる。
var outerValue = 'ninja';
function outerFunction() {
console.log(outerValue);
}
outerFunction();
クロージャの働きがわかりやすい例は以下のようになる。
var outerValue = 'samurai';
var later;
function outerFunction() {
var innerValue = 'ninja';
function innerFunction() {
console.log(outerValue);
console.log(innerValue);
}
later = innerFunction;
}
outerFunction();
later();
outerFunctionの呼び出しによって値がセットされたinnerValueは、呼び出しの終了によってメモリ上から消えてしまい、最後のlater()によるinnerFunctionの呼び出しの時点ではundefined
になっているようにも思える。
しかし、実際にlater()を実行すると、innerValueには’ninja’が入っている。
実は、innerFunctionを定義するとき、関数宣言を定義するだけでなく、関数定義の時点のスコープ内にある全ての変数を包含するクロージャが作成されているのだ。ここで重要なのは、クロージャ経由で外部の変数にアクセスする場合、その変数はクロージャが破棄されるまでメモリ上に残り続けるという点である。
クロージャを動かす
プライベート変数(のようなもの)
JavaScriptにはプライベート変数はないが、クロージャを使うことで似たようなものを作り出すことができる。
function Ninja() {
var feints = 0;
this.getFeints() = function() {
return feints;
};
this.feint = function() {
feints++;
}
}
var ninja1 = new Ninja();
ninja1.feint();
console.log(ninja1.feints); // undefined
console.log(ninja1.getFeints()); // 1
var ninja2 = new Ninja();
console.log(ninja2.getFeints()); // 0
クロージャをコールバックとして使う
setInterval(function() {
// doSomething
}, 100);
実行コンテキストによるコード実行の追跡
JavaScriptにおいて、実行の基本的な単位は関数である。JavaScriptのコードは、関数が関数を呼び、そして呼び出し元に戻っていくような流れで実行される。このとき、JavaScriptエンジンは関数呼び出しのスタックを記憶している。
コードがJavaScriptエンジンによって実行されるとき、それぞれの文は一定の実行コンテキストによって実行される。実行コンテキストには、グローバルコンテキストと関数実行コンテキストの2種類がある。
- グローバルコンテキストは1つだけ存在する
- 関数実行コンテキストは関数呼び出しのたびに作成される
関数呼び出しのたびに実行コンテキストが作成されるが、その関数の処理が終わると元の場所に復帰する必要がある。そのため、関数内でさらに関数が呼ばれるような場合には、関数の実行コンテキストがスタックとしてメモリ上に保持される。
function skulk(ninja) {
report(ninja + " skulking");
}
function report(message) {
console.log(message);
}
skulk("Kuma"); // グローバルコンテキストからの関数呼び出し
// 1. グローバルコンテキストが作成される
// 2. skulk関数の実行コンテキストがスタックに積まれる
// 3. report関数の実行コンテキストがスタックに積まれる
// 4. report関数の実行コンテキストが破棄され、skulk関数に復帰する
// 5. skulk関数の実行コンテキストが破棄され、グローバルコンテキストに復帰する
関数の実行スタック(Call Stack)は、Chrome DevTools等を活用することで簡単に確認できる。
レキシカル環境(lexical environments)で識別子を追いかける
レキシカル環境(lexical environments)とは、JavaScriptエンジンの内部の構成要素で、識別子と変数のマッピングを追いかけるのに使われる。
var ninja = "Hattori";
console.log(ninja);
上記コードで、レキシカル環境は、console.log
文の際にninja
変数について尋ねられる。レキシカル環境は、一般にはスコープと呼ばれる。
通常、レキシカル環境はJavaScriptコードの特定の構造と結びついている。すなわち、関数、コードのブロック、try-catch
文のcatch
部分である。これらの構造は独自の識別子マッピングを持つことができる。
コードのネスト
レキシカル環境は、多くの場合、コードのネストに基づいている。
const ninja = 'Muneyoshi';
// skulk関数はグローバル
function skulk() {
const action = 'skulking';
// report関数はskulk関数の内側
function report() {
const reportNum = 3;
// forループはreport関数の内側
for (let i = 0; i < reportNum; i++) {
console.log(ninja + ' ' + action + ' ' + i);
}
}
}
skulk関数を呼び出すたびに、新しいレキシカル環境が作成される。
重要なのは、内側のコードは外側でアクセスされた変数にアクセス可能なことである。
コードのネストとレキシカル環境
ローカル変数、関数宣言、関数パラメーターの追跡に加えて、それぞれのレキシカル環境は外部のレキシカル環境も追跡しなければならない。JavaScriptでは、関数をファーストクラスオブジェクトにすることで、外部の環境の追跡を可能にしている。
関数が作成されると、関数が作成されたレキシカル環境への参照が、 [[Environment]]
という内部プロパティとして保持される。二重の角カッコは、JavaScriptエンジンが内部で使用するプロパティの印であり、プログラムから直接触ることはできない。
関数が実行されるたびに、新しい関数の実行コンテキストが作成され、スタックに積まれる。加えて、新しく関連するレキシカル環境が作成される。新しく作成されるレキシカル環境の外部環境のために、JavaScriptエンジンは関数の内部の [[Environment]]
プロパティから環境を取り出す。これによって、新しく実行される関数のための環境が用意される。
JavaScript変数の種類を理解する
JavaScriptでは、3種類のキーワードを使って変数を定義できる。var
, let
, const
である。これらは (1) 変更可能性(mutability) (2) レキシカル環境との関係 という2点で異なっている。
変数の変更可能性(mutability)
変数宣言のキーワードを変更可能性で分類するなら、const
は変更不可、var
とlet
は変更可、となる。const
で宣言された変数は変更不可(immutable)となり、値の再代入ができない。一方、var
またはlet
によって宣言された変数は何度でも値を再代入することができる。
const変数
const
変数には、宣言と同時に初期値を設定する必要がある。const
変数は以下のような目的で使用される。
- 再代入すべきでない変数を明示する
- 固定された値を定義する
const
変数には再代入ができないため、意図しない変更から保護されている。また、JavaScriptエンジンによる最適化も可能になる。再代入の不要な変数(多くは不要なはず)に対しては、const
を使って宣言することが推奨される。
なお、const
で宣言した変数は再代入ができないだけで、オブジェクトのプロパティを変更したり、配列の要素を追加・削除したりすることは可能である。オブジェクトや配列の変更を禁止したい場合はObject.freeze
メソッドを使用する。
// strictモードでないと、freezeしたオブジェクトへの代入はエラーにはならず、無視される
'use strict';
const job = 'samurai';
try {
job = 'ninja';
console.log(job);
} catch (e) {
console.log('error');
}
// => error
const ninja = {};
try {
ninja.weapon = 'katana';
console.log(ninja.weapon);
} catch (e) {
console.log('error');
}
// => katana
Object.freeze(ninja);
try {
ninja.weapon = 'kunai';
console.log(ninja.weapon);
} catch (e) {
console.log('error');
}
// => error
const weapons = [];
weapons.push('kunai');
// 配列のfreezeにもObject.freezeを使う
Object.freeze(weapons);
try {
weapons.push('shuriken');
console.log(weapons);
} catch (e) {
console.log('error');
}
// => error
変数定義キーワードとレキシカル環境
レキシカル環境との関係(スコープ)の観点で分類するなら、var
とlet
, const
の間で線が引かれる。
varキーワードの使用
var
キーワードを使用するとき、変数は最寄りの関数かグローバルなレキシカル環境に保存される(ブロックのレキシカル環境は無視されることに注意!)。
for (var i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // iにはforループの外でもアクセスできる
let, constを使ってブロックスコープ変数を定義する
let
, const
を使って定義した変数は最寄りのレキシカル環境に保存される。もし最寄りがブロックのレキシカル環境であれば、そこに保存される。
for (let i = 0; i < 3; i++) {
const message = 'messge:' + i;
console.log(message);
}
console.log(typeof i, typeof message);
// => undefined, undefined
再代入可能な変数には let
を使うことで、よりスコープの狭い変数を定義することができる。後方互換性以外の理由でvar
を使う理由はない。
レキシカル環境に識別子を登録する
識別子の登録過程
JavaScriptコードの実行は2段階に分けて行われる。
最初のフェーズは新しいレキシカル環境が作成されるたびに実行される。このフェーズでは、コードは実行されず、JavaScriptエンジンは変数と関数の宣言を探して現在のレキシカル環境に登録する。2番目のフェーズでは、コードが実行される。このフェーズは以下のように詳細化できる。
- 関数環境を作成する場合、
arguments
識別子を作成する - 関数またはグローバル環境を作成する場合、コードを走査して関数宣言を探し、識別子を登録する
- 変数宣言のための走査が行われ、
var
で宣言された変数は最寄りの関数の、let
またはconst
で宣言された変数は最寄りのブロックのレキシカル環境に登録される
関数宣言より前に関数を実行する
- 関数宣言を使用して定義された関数には宣言より前でもアクセスできる
- 関数式やアロー関数はできない
関数の上書き
関数宣言を使用すると、その関数を参照する変数はコードの実行よりも先に定義される。
console.log(typeof fun); // function
var fun = 3;
console.log(typeof fun); // number
function fun(){}
console.log(typeof fun); // number
まとめ
- クロージャによって、その関数が定義されたスコープ内にある全ての変数にアクセス可能になる
- クロージャは疑似プライベート変数やコールバック関数に利用できる
- JavaScriptエンジンは実行スタックによって関数の実行を追跡する
- JaaScriptエンジンはレキシカル環境(スコープ)によって識別子を追跡する
- JavaScriptでは、グローバル・関数・ブロックのいずれかのスコープの変数が定義できる
- 変数の定義には
var
,let
,const
キーワードが使用できる - クロージャはJavaScriptのスコープのルールの副作用である