スコープとクロージャ
1. スコープとは
プログラミング言語において、変数の格納とアクセス能力がプログラムに状態をもたらします。
変数がどこに格納されているか、そしてプログラムがどのように変数を見つけるかに関するルールがスコープと呼ばれます。
1.1 コンパイル原理
すべてのJavaScriptコードは実行前にコンパイルプロセス(通常は直前)を経ます。
- トークン化/語彙解析(Tokenizing/Lexing)
文字列から意味のあるコードの塊(トークン)に分解します。
- 構文解析(Parsing)
トークンの流れを、プログラムの構文構造を表す階層的な木(抽象構文AST)に変換します。
- コード生成
ASTを実行可能なコードに変換します。
1.2 JavaScriptのコンパイル原理
- エンジン - JavaScriptプログラムのコンパイルと実行プロセスを担当
- コンパイラ - 構文解析とコード生成を担当
- スコープ - すべての変数(識別子)のクエリを収集・維持し、現在実行中のコードがこれらの識別子にアクセスできるルールを決定
変数の代入操作は2つのアクションを実行します:
- コンパイラが現在のスコープで変数を宣言する(以前に宣言されていない場合)
- 実行時エンジンがスコープ内でその変数を検索し、見つかれば値を代入する
① エンジンは変数をどのように検索するか
エンジンはLHSクエリとRHSクエリを使用して変数を検索します。
変数が代入操作の左側に現れる場合はLHSクエリ(格納位置を見つけて代入)、右側に現れる場合はRHSクエリ(その具体的な値を見つける)を実行します。
簡単に言えば:左は位置を探し、右は値を探す
② LHSとRHSの練習
function processData(param) {
// 2. LHSでparamの位置を見つけ、値10を代入
let result = param; // 3. RHSでparamの値を取得 4. LHSでresultの位置を見つけ、paramの値10を代入
return param + result; // 5. RHSでparamの値を取得 6. RHSでresultの値を取得
}
let output = processData(10) // 1. RHSでprocessDataの値を取得 7. LHSでoutputの位置を見つけ、processData(10)の値20を代入
③ 補足
ここでのLHSとRHSはgetterとsetterを連想させます。
LHSはsetterに相当し、その位置を見つけて値を設定します。
RHSはgetterに相当し、その値を取得します。
1.3 スコープチェーン
スコープのネストが発生した場合、現在のスコープで特定の変数が見つからないと、エンジンは外側のネストされたスコープで継続して検索し、その変数が見つかるか、最も外側のスコープ(グローバルスコープ)に到達するまで続けます。
1.4 RHSとLHSの検索失敗
RHSクエリがすべてのネストされたスコープで必要な変数を見つけられない場合、エンジンはReferenceError例外をスローします。
エンジンがLHSクエリを実行し、最上位(グローバルスコープ)でも目標の変数が見つからない場合、グローバルスコープにその名前を持つ変数が作成され、エンジンに返されます。
厳密モードでは、LHSクエリが失敗した場合、グローバル変数が作成・返されることはなく、エンジンはRHSクエリ失敗時と同様のReferenceError例外をスローします。
1.5 まとめ
- スコープは、変数(識別子)の検索場所と方法を決定する一連のルールです
- 検索の目的が変数への代入である場合、LHSクエリが使用されます。目的が変数の値の取得である場合、RHSクエリが使用されます
- LHSとRHSの両方のクエリは、現在の実行スコープから始まり、グローバルスコープに至るまでスコープをたどって検索します
- LHSで見つからない場合はエラーになりませんが、RHSで見つからない場合はエラーになります
2. JavaScriptのスコープ
2.1 語彙的スコープ
スコープには主に2つの動作モデルがあります:語彙的スコープと動的スコープ。
JavaScriptは語彙的スコープを採用しています。
語彙的スコープとは、語彙段階で定義されるスコープのことです。
語彙的スコープは、コードを記述する際に変数とブロックスコープがどこに書かれているかによって決定されます。そのため、語彙解析器がコードを処理する際、スコープは変更されません。
語彙的スコープは、関数がどこで宣言されたかによってその語彙的スコープが決定されることを意味します。関数がどこで呼び出されようと、どのように呼び出されようと、その語彙的スコープは宣言された場所によってのみ決定されます。
2.2 関数スコープ
関数スコープの意味は、この関数に属するすべての変数が関数全体の範囲で使用および再利用できるということです(実際にはネストされたスコープでも使用可能)。
関数スコープの利点は、内部実装を隠蔽できることです。外部スコープからはラップ関数の内部の内容にアクセスできません。
関数宣言と関数式について
もし`function`が宣言の最初の単語であるなら、これは関数宣言であり、そうでなければ関数式です。
即時実行関数式(IIFE)
(function createModule(){
//...
})()
(function(){
//...
}())
(function initFramework(global){
//...
})(window)
2.3 ブロックスコープ
ES6では、`let`と`const`で宣言された変数はブロックスコープを持っています。
try/catchにおけるブロックスコープ
`try/catch`の`catch`節はブロックスコープを作成し、そこで宣言された変数はcatch内部でのみ有効です。
2.4 まとめ
あるスコープ内で宣言された変数は、そのスコープに属します。
3. 変数の巻き上げ
コンパイル段階の一部の作業は、すべての宣言を見つけ、それらを適切なスコープに関連付けることです。
変数と関数を含むすべての宣言は、コードが実行される前に最初に処理されます。
関数の巻き上げは変数よりも先に行われます。
宣言自体は巻き上げられますが、関数式を含む代入操作は巻き上げられません。
4. スコープとクロージャ
クロージャは、語彙的スコープに基づいてコードを記述した際に生じる自然な結果です。
関数がその属する語彙的スコープを記憶し、アクセスできる場合、クロージャが生成されます。たとえ関数が現在の語彙的スコープの外側で実行されてもです。
言い換えると、変数の検索は関数が定義された場所で上位スコープを検索し、実行された場所ではありません。
関数を戻り値として使用する例
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
ループとクロージャ
function setupButtons() {
const buttons = [];
for (let i = 1; i <= 3; i++) {
buttons.push(function() {
console.log(`Button ${i} clicked`);
});
}
return buttons;
}
const buttonActions = setupButtons();
buttonActions[0](); // Button 1 clicked
buttonActions[1](); // Button 2 clicked
buttonActions[2](); // Button 3 clicked
モジュールパターン
const Calculator = (function() {
let result = 0;
function add(value) {
result += value;
return this;
}
function subtract(value) {
result -= value;
return this;
}
function getResult() {
return result;
}
function reset() {
result = 0;
return this;
}
return {
add,
subtract,
getResult,
reset
};
})();
Calculator.add(5).subtract(2);
console.log(Calculator.getResult()); // 3
Calculator.reset();
console.log(Calculator.getResult()); // 0