JavaScript は非常に関数指向な言語です。それは我々に多くの自由を与えます。ある時点で作成した関数は、別の変数にコピーしたり別の関数に引数として渡し、後でまったく別の場所から呼ぶことができます。
私たちは関数が外側の変数にアクセスできることを知っています。そしてこの特徴は頻繁に使われます。
しかし、外部の変数が変わると何が起きるでしょう?関数は最新の値を得るでしょうか?それとも関数が生成された時点で存在していた値を取得することになるでしょうか?
また、関数がコード内の別の場所に移動して、そこから呼び出されたとき、何が起きるでしょう?新しい場所での外部変数にアクセスできるのでしょうか?
言語によっては、ここで説明する内容とは異なる振る舞いをします。このチャプターでは、JavaScript について説明します。
いくつかの質問
最初に2つの状況を考え、内部の仕組みを少しずつ学んで行きましょう。そうすれば、次の質問や、今後出てくるであろうより複雑なものにも答えることができるでしょう。
-
関数
sayHi
は外部変数name
を使います。関数を実行するとき、2つの値のうち、どちらが使われるでしょうか?let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // 何が表示されますか?: "John" それとも "Pete"?
このような状況は、ブラウザやサーバサイドでの開発両方で一般的です。関数は作られた時間よりも後、例えばユーザ操作やネットワークリクエストの後に実行がスケジュールされる場合があります。
従って、質問は: 関数
sayHi
は最新の変更を受け取りますか? -
関数
makeWorker
は別の関数を作り、それを返します。その新しい関数は他の場所から呼び出すことができます。作成された場所、呼び出し場所、あるいはその両方からの外部変数にアクセスできますか?function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // 関数を作成する let work = makeWorker(); // それを呼ぶ work(); // 何が表示されますか? "Pete" (作成された場所の name ) or "John" (呼び出された場所の name )?
レキシカル/語彙環境(Lexical Environment)
何が起きるか理解するために、まず “変数” が技術的に何であるかを議論しましょう。
JavaScript では、すべての実行中の関数やコードブロック、スクリプト全体は レキシカル環境 と呼ばれる関連オブジェクトを持っています。
レキシカル環境オブジェクトは2つの部分から構成されます:
- 環境レコード(Environment Record) 。プロパティとしてすべてのローカル変数をもつオブジェクトです(
this
の値など、他の情報もいくらか持っています)。 - 外部のレキシカル環境 への参照。通常、直近の外部のレキシカルなコードに関連付けられています(現在の波括弧の外側)。
なので、“変数” は単に、特別な内部オブジェクト、環境レコードのプロパティです。“変数を取得または変更する” とは、“そのオブジェクトのプロパティを取得または変更する” ことを意味します。
例えば、この簡単なコードでは、レキシカル環境は1つだけあります。:
これは、スクリプト全体に関連付けられた、いわゆるグローバルレキシカル環境です。 ブラウザの場合、すべての <script>
タグは同じグローバル環境を共有します。
上の図の長方形は環境レコード(変数ストア)で、矢印(outerの部分)は外部参照を意味します。グローバルレキシカル環境は外部参照を持っていないので、 null
です。
下記は、let
変数がどのように動作するかを示す図です:
右側の長方形は、実行の間でどのようにグローバルレキシカル環境が変わるかを示しています。:
- スクリプト開始時、レキシカル環境は空です。
let phrase
定義が現れました。今は初期値がないので、undefined
が格納されます。phrase
が代入されます。phrase
が新しい値を参照します。
今のところすべてシンプルに見えますね。
要約すると:
- 変数は特別な内部オブジェクトのプロパティで、現在の実行ブロック/関数/スクリプトと関連付けられています。
- 変数を使った作業は、実際にはそのオブジェクトのプロパティを使って作業しています。
関数宣言
関数宣言は特別です。let
変数とは異なり、実行がそこに到達したときに処理されるのではなく、レキシカル環境が作られたときに処理されます。グローバルレキシカル環境では、スクリプトが開始される瞬間を意味します。
そういうわけで、定義される前に関数宣言を呼び出すことができます。
下のコードはレキシカル環境は最初から空ではないことを示しています。関数宣言なので say
を持っています。その後 let
で宣言された phrase
を取得します:
内外のレキシカル環境
呼び出しの中で、say()
は外部変数を使います。何が起きているか見てみましょう。
まず、関数を実行するとき、新しい関数のレキシカル環境が自動的に作られます。それはすべての関数での一般的なルールです。そのレキシカル環境はローカル変数や呼び出しパラメータを格納するために使われます。
これは、実行が say("John")
の内部にあるときで、矢印のついた行にあるレキシカル環境の図です。:
関数呼び出しの間、2つのレキシカル環境があります: 内部のもの(関数呼び出し用)と、外部のもの(グローバル)です:
- 内部のレキシカル環境は現在の
say
の実行に対応しています。1つ変数(name
)を持っており、それは関数の引数です。私たちはsay("John")
を呼び出したので、name
は"John"
です。 - 外部のレキシカル環境はグローバルレキシカル環境です。
内部のレキシカル環境は外部のものへの 外部
参照を持っています。
コードが変数にアクセスしたいとき、最初に内部のレキシカル環境を探します。その次に外側を探し、チェーンの最後になるまで繰り返します。
もし変数がどこにもない場合、strict モードではエラーになります。use strict
がなければ、未定義変数への代入は下位互換性のために新しいグローバル変数を作成します。
今回の例でどのように探索されるか見てみましょう:
say
の内側にあるalert
がname
にアクセスしたいとき、関数のレキシカル環境の中からすぐに見つけます。phrase
にアクセスしたいとき、ローカルにはphrase
がないので、続いて外部
参照を行い、グローバルでそれを見つけます。
これで、このチャプターの最初にあった1つ目の質問に答えることができます。
関数は外部変数を最新の値として取得します。
これは、説明されたメカニズムによるものです。 古い変数の値はどこにも保存されません。 関数がそれらを必要とするとき、自身または外部のレキシカル環境から現在の値を取ります。
従って、最初の質問の答えは Pete
です:
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete"; // (*)
sayHi(); // Pete
上のコードの実行フローは次の通りです:
- グローバルレキシカル環境は
name
を持っています:"John"
. (*)
の行でグローバル変数が変更され、今name
は"Pete"
です。- 関数
say()
が実行され、外部からname
を取得します。ここでは、それはグローバルレキシカル環境であり、"Pete"
です。
新しい関数のレキシカル環境は、関数が実行するたびに作られることに注意してください。
また、関数が複数回呼び出された場合、各呼び出しには自身のレキシカル環境があり、ローカル変数とその実行に固有のパラメーターがあります。
“レキシカル環境” は仕様上のオブジェクトです。コードからそのオブジェクトを取得したり、直接操作することはできません。JavaScriptエンジンはメモリを節約するために使っていない変数を破棄したり、他の内部トリックを行うと言った最適化をする場合がありますが、見える振る舞いは上で説明した通りである必要があります。
ネストされた関数
関数は、別の関数の内部で作成されたとき、“ネストされた” と呼ばれます。
技術的には、それは簡単に可能です。
私たちは、コードを整理するのに利用します:
function sayHiBye(firstName, lastName) {
// 下で使うネストされたヘルパー関数です
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
ここでの ネストされた 関数 getFullName()
は利便性のために作られています。それは外部変数にアクセスすることができるので、フルネームを返すことができます。
さらに興味深い点は、新しいオブジェクトのプロパティ(外部関数がメソッドを持つオブジェクトを作成する場合)またはその自身の結果として、ネストされた関数を返すことができることです: それは他の場所で使うことができます。どこにいても、同じ外部変数には依然としてアクセスできます。
コンストラクタ関数を持つ例です(チャプター コンストラクタ 演算子 new を参照):
// コンストラクタ関数は新しいオブジェクトを返します
function User(name) {
// オブジェクトメソッドはネストされた関数として作成されます
this.sayHi = function() {
alert(name);
};
}
let user = new User("John");
user.sayHi(); // このメソッドは外部の "name" へアクセスできます
関数を返す例です:
function makeCounter() {
let count = 0;
return function() {
return count++; // 外の counter へアクセスできます
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
makeCounter
の例を続けましょう。それは各呼び出しで次の数値を返す “counter” 関数を作ります。シンプルであるにも関わらず、そのコードからわずかに変更されたバリアントは実践で使われています。例えば、擬似乱数生成器(pseudorandom number generator) などです。なので、この例はあまり不自然ではありません。
counter は内部でどのように動作しているのでしょう?
内部関数が実行されると、count++
の変数は内側から外に検索されます。上の例では、その順番は次のようになります。:
- ネストされた関数のローカル変数
- 外部関数の変数
- …そしてさらにグローバルに到達するまで
この例では、count
はステップ 2
で見つかります。外部変数が変更されると、それが見つかった場所で変更されます。従って、count++
は外部変数を見つけ、それが属するレキシカル環境内でその値を増やします。
ここで2つの質問があります:
counter
をmakeCounter
に属していないコードからリセットすることはできるでしょうか? 例えば、上記の例でalert
呼び出し後で。- もし
makeCounter()
を複数回呼び出した場合、それは複数のcounter
関数を返します。それらは独立していますか?それとも同じcount
を共有しますか?
続きを読む前に考えてみてください。
…答えはでましたか?
では、答えをみていきましょう。
- 方法はありません。
counter
はローカル関数の変数であり、外部からアクセスすることはできません。 - すべての
makeCounter()
の呼び出しで、自身のcounter
を持つ新しい関数のレキシカル環境が作られます。従って、counter
関数の結果は独立します。
デモです:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter1 = makeCounter();
let counter2 = makeCounter();
alert( counter1() ); // 0
alert( counter1() ); // 1
alert( counter2() ); // 0 (独立))
外部変数の状況は明らかになってきたと思います。しかし、より複雑な状況では、内部のより深い理解が必要になります。なので次に進みましょう。
環境の詳細
今や、クロージャが一般的にどのように動作するかを理解したので、重要なポイントまで降りることができます。
これは makeCounter
の例で起こっていることを段階的に示したものです。すべてを理解するよう、順に見てください。まだ説明していない追加の [[Environment]]
プロパティに注意してください。
-
スクリプトが開始された直後は、グローバルレキシカル環境だけがあります:
開始時点では、
makeCounter
関数だけがあります。なぜなら、それが関数宣言だからです。それはまだ実行されていません。“誕生した” すべての関数は、作成されたレキシカル環境への参照を持つ隠しプロパティ
[[Environment]]
を受け取ります。まだこれについて話していませんでしたが、このプロパティは関数が作られた場所を知る方法です。ここで、
makeCounter
はグローバルレキシカル環境に作られるので、[[Environment]]
はそこへの参照を維持します。つまり、関数にはそれが生まれたレキシカル環境への参照が “刻み込まれて” います。
[[Environment]]
は、その参照を持つ隠し関数プロパティです。 -
次に、コードが実行され
makeCounter()
の呼び出しが行われます。これは、実行がmakeCounter()
の処理の最初の行の時点の図です。makeCounter()
呼び出しの時点で、その変数や引数を保持するためにレキシカル環境が作られます。すべてのレキシカル環境では2つのものを保持します:
- ローカル変数を持つ環境レコード。我々のケースでは、
count
は唯一のローカル変数です(let count
の行が実行されたとき出現します)。 - 外部のレキシカルへの参照。それは関数の
[[Environment]]
にセットされています。ここではmakeCounter
の[[Environment]]
はグローバルレキシカル環境を参照します。
従って、今は 2つのレキシカル環境があります。1つ目はグローバル、2つ目はグローバルへの外部参照を持つ現在の
makeCounter
呼び出しです。 - ローカル変数を持つ環境レコード。我々のケースでは、
-
makeCounter
の実行中、小さいネストされた関数が作られます。関数が関数宣言、関数式どちらを使って作られたのかは関係ありません。すべての関数は、作成されたレキシカル環境を参照する
[[Environment]]
プロパティを取得します。従って、小さなネストされた関数も同様に取得します。新しいネストされた関数の
[[Environment]]
の値はmakeCounter()
の現在のレキシカル環境です。:このステップでは内部関数が作られますが、まだ呼ばれていないことに注意してください。
function() { return count++; }
の内側のコードは実行されておらず、私たちはそのコードを返します。 -
実行が進み、
makeCounter()
呼び出しが終わると、結果(小さなネストされた関数)がグローバル変数counter
に代入されます。関数は1行
return count++
だけです。これは呼び出されたときに実行されます。 -
counter()
が呼ばれると、“空の” レキシカル環境が作られます。それはローカル変数を持っていませんが、counter
の[[Environment]]
はその外部参照として使われるので、それが作られた場所である、前のmakeCounter()
呼び出しの変数にアクセスすることができます。いま、変数にアクセスすると、最初に自身のレキシカル環境を探します(空です)。次に前の
makeCounter()
呼び出しのレキシカル環境、次にグローバルです。count
を探すとき、最も近い外部のレキシカル環境、つまりmakeCounter
変数の中でそれを見つけます。ここでどのようにメモリ管理がされているか注意してください。
makeCounter()
呼び出しが少し前に終わったとき、その[[Environment]]
を参照するネストされた関数があるので、そのレキシカル環境はメモリに保持されています。一般的に、レキシカル環境オブジェクトは、それを使用する可能性のある関数が存在する限り存続します。 それがなければクリアされます。
-
counter()
の呼び出しはcount
の値を返すだけでなく、その値も増やします。変更は “その場” で行われることに注目してください。count
の値は、正確に見つかった環境で変更されます。なので、変更(
count
の新しい値)だけ前のステップに戻ります。以降の呼び出しはすべて同じことを行います。 -
次の
counter()
呼び出しも同じです。
これで、このチャプターの最初にあった、2つ目の質問への答えも明らかになったはずです。
下のコードの work()
関数は、その起源の場所から外部のレキシカル環境への参照を通じて name
を得ようとします。:
従って、結果は "Pete"
です。
しかし、もし makeWorker()
に let name
がなかった場合は外側へ探しに行き、上のチェーンで分かるようにグローバル変数を取得します。この場合、結果は "John"
になります。
開発者が一般的に知っておくべき、一般的なプログラミング用語 “クロージャ” があります。
クロージャ(closure) は外部変数を記憶し、それらにアクセスできる関数です。いくつかの言語ではそれは不可能、もしくはそれを実現するために特別な方法で関数を書く必要があります。しかし、上で説明したとおり、JavaScriptにおいては、すべての関数は自然にクロージャです(1つだけ例外があります。それについては "new Function" 構文 で説明します)。
つまり: それらは隠された [[Environment]]
プロパティを使ってどこに作成されたのかを自動的に覚えていて、すべてが外部変数にアクセスできます。
面接でフロントエンドの開発者が「クロージャは何ですか?」という質問を受けたとき、有効な回答は、クロージャの定義と、JavaScriptにおいてはすべての関数がクロージャであること、また [[Environment]]
プロパティとレキシカル環境の仕組みと言った技術的に詳細な用語です 。
コードブロックとループ、IIFE(即時実行関数)
上の例では関数に焦点を当てていました。しかしレキシカル環境はコードブロック {...}
にも存在します。
コードブロックが実行され、ブロック内のローカル変数を含むときそれらが作られます。ここではいくつかの例を示します。
If
下の例では、実行が if
ブロックに来たとき、新しい “ifだけの” レキシカル環境が作られます。:
新しいレキシカル環境はその外部への参照をもつため phrase
を見つけることができます。しかし、if
の中で宣言されたすべての変数と関数式はレキシカル環境の中にあり、外部からは見えません。
例えば、if
が終わった後にある alert
は、 user
が見えないため、エラーになります。
For, while
ループでは、すべての実行が別々のレキシカル環境を持っています。もし変数が for
で宣言されると、そのレキシカル環境のローカル変数になります:
for (let i = 0; i < 10; i++) {
// 各ループはそれぞれのレキシカル環境を持ちます
// {i: value}
}
alert(i); // Error, このような変数はありません
これは例外です。なぜなら、let i
は視覚的に {...}
の外にあるからです。しかし、実際にはループの各実行はその中に現在の i
を持つレキシカル環境を持ちます。
ループの後、i
は見えません。
コードブロック
また、変数を “ローカルスコープ” に分離するため、“裸の” コードブロック {…}
を使うこともできます。
例えば、Webブラウザでは、すべてのスクリプトは同じグローバル領域で共有されています。従って、もしもあるスクリプトの中にグローバル変数を作ると、他からも利用可能になります。が、2つのスクリプトが同じ変数名を使っていたりお互いにオーバライドしている場合、ソースの衝突が起きます。
変数名が広く使われていて、スクリプトの作者が互いを認識していない場合に起こります。
これを回避したい場合は、コードブロックを使用して、スクリプト全体またはスクリプト内の領域を分離することができます。:
{
// 外には見せるべきでないローカル変数で必要な処理をする
let message = "Hello";
alert(message); // Hello
}
alert(message); // Error: message は未定義
ブロックの外のコード(もしくは別のスクリプト)はその中の変数が見えません。なぜならコードブロックは自身のレキシカル環境を持つからです。
IIFE(即時実行関数)
古いスクリプトでは、いわゆる “即時実行関数(immediately-invoked function expressions)” (IIFEと略します) がこの目的に使われます。
このようなものです。:
(function() {
let message = "Hello";
alert(message); // Hello
})();
関数式が作られすぐに呼ばれます。従って、コードはすぐに実行され自身のプライベート変数を持ちます。
関数式は括弧 (function {...})
で囲まれています。なぜなら、JavaScriptはメインコードフローの中で "function"
を見つけると、関数宣言の開始と理解します。しかし、関数宣言は名前が必須なので、エラーになります。
:
// Error: Unexpected token (
function() { // <-- JavaScript は関数名を見つけることができません (エラーになります)
let message = "Hello";
alert(message); // Hello
}();
“わかった、では関数宣言にして名前をつけよう” と思うかもしれませんが、これは動作しません。JavaScriptでは関数宣言をすぐに呼ぶことができません。:
// 下の括弧による構文エラー
function go() {
}(); // <-- 関数宣言は即時呼び出しできません
従って、関数が別の式のコンテキストで作られており、関数式であることを JavaScript に示すには括弧が必要になります。名前なしで、すぐに呼び出せる必要があります。
JavaScriptには、関数式を意味する他の方法があります:
// IIFE の作成方法
(function() {
alert("関数を括弧で囲みます");
})();
(function() {
alert("全体を括弧で囲みます");
}());
!function() {
alert("NOT 演算子は式を開始します");
}();
+function() {
alert("単項プラスは式を開始します");
}();
上のすべてのケースで、私たちは関数式を宣言した後すぐに実行することができます。
ガベージコレクション
私たちが話してきたレキシカル環境オブジェクトは、通常の値と同じメモリ管理ルールの対象です。
-
通常、レキシカル環境は関数が実行された後にクリーンアップされます。例:
function f() { let value1 = 123; let value2 = 456; } f();
ここで2つの値は技術的にレキシカル環境のプロパティです。しかし
f()
が終わった後、レキシカル環境は到達不能になるので、メモリから削除されます。 -
…しかし、もし
f
の後でもまだ到達可能なネストされた関数がある場合、その[[Environment]]
参照は外部のレキシカル環境を生かし続けます。:function f() { let value = 123; function g() { alert(value); } return g; } let g = f(); // g は到達可能であり、メモリ上で外部のレキシカル環境で保持されます
-
もし
f()
が何度も呼ばれ、結果の関数が保持される場合、対応するレキシカル環境オブジェクトもまたメモリに残ります。下のコードでは3つすべて:function f() { let value = Math.random(); return function() { alert(value); }; } // 配列に3つの関数があり、それぞれが対応する f() からの // レキシカル環境と関連づいています // LE LE LE let arr = [f(), f(), f()];
-
レキシカル環境オブジェクトは到達不能になったときに死にます。つまり: それを参照するネストされた関数が残っていないときです。下のコードでは、
g
が到達不能になった後、value
もまたメモリからクリアされます:function f() { let value = 123; function g() { alert(value); } return g; } let g = f(); // g が生きている間 // 対応するレキシカル環境も生きている g = null; // ...今、メモリはクリーンアップされます
現実の最適化(Real-life optimizations)
これまで見てきたように、理論的には関数が生きている間、すべての外部変数も保持されます。
しかし、実際にはJavaScriptエンジンはそれを最適化しようとします。変数の使用状況を分析し、外部変数が使用されていないことがわかりやすい場合は削除されます。
V8(Chrome, Opera)の重要な副作用はこのような変数はデバッグでは利用できなくなることです。
Chromeで Developer Tools を開いて下の例を実行してみてください。
一時停止したとき、console で alert(value)
をタイプしてください。
function f() {
let value = Math.random();
function g() {
debugger; // in console: type alert( value ); No such variable!
}
return g;
}
let g = f();
g();
ご覧の通り、このような変数はありません! 理論的にはアクセスできるはずですが、エンジンが最適化しています。
これは興味深い(そうでなくても時間のかかる)デバッグの問題につながる可能性があります。 期待しているものの代わりに、同じ名前の外部変数を見ることができます:
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // in console: type alert( value ); Surprise!
}
return g;
}
let g = f();
g();
V8 のこの機能は知っておくと良いです。もしも Chrome/Opera でデバッグしている場合、遅かれ早かれこれに遭遇するでしょう。
これはデバッガのバグではなく、V8の特別な機能です。時々変わるかもしれません。 このページの例を実行することで、いつでもチェックすることができます。
コメント
<code>
タグを使ってください。複数行の場合は<pre>
を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。