深層 JavaScript
本書をサポートしてください:購入 または 寄付
(広告、ブロックしないでください。)

4 環境:変数の内部動作



この章では、ECMAScript言語仕様が変数をどのように扱うかを詳しく見ていきます。

4.1 環境:変数を管理するためのデータ構造

環境とは、ECMAScript仕様が変数を管理するために使用するデータ構造です。これは、キーが変数名で、値がそれらの変数の値である辞書です。各スコープには、関連付けられた環境があります。環境は、変数に関連する以下の現象をサポートできる必要があります。

それぞれの現象について、例を用いて説明します。

4.2 環境による再帰

最初に再帰に取り組みます。次のコードを考えてください。

function f(x) {
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  return f(tmp);
}
assert.equal(g(3), 8);

各関数呼び出しに対して、呼び出された関数の変数(パラメータとローカル変数)用の新しい記憶領域が必要です。これは、いわゆる実行コンテキストのスタックによって管理されます。実行コンテキストは環境への参照です(この章の目的において)。環境自体はヒープ上に格納されます。これは、実行がスコープを離れた後も、環境が時々存続するためです(クロージャを検討する際に確認します)。したがって、それ自体はスタックによって管理できません。

4.2.1 コードの実行

コードを実行する間、次のポーズを行います。

function f(x) {
  // Pause 3
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  // Pause 2
  return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);

何が起こるかを示します。

図1:再帰、ポーズ1 – `g()` を呼び出す前:実行コンテキストスタックには1つのエントリがあり、最上位レベルの環境を指しています。その環境には、`f()` と `g()` の2つのエントリがあります。
図2:再帰、ポーズ2 – `g()` を実行中:実行コンテキストスタックの先頭は、`g()` のために作成された環境を指しています。その環境には、引数 `y` とローカル変数 `tmp` のエントリが含まれています。
図3:再帰、ポーズ3 – `f()` を実行中:実行コンテキストの先頭は、`f()` の環境を指しています。

4.3 環境によるネストされたスコープ

ネストされたスコープが環境によってどのように実装されているかを調べるために、次のコードを使用します。

function f(x) {
  function square() {
    const result = x * x;
    return result;
  }
  return square();
}
assert.equal(f(6), 36);

ここでは、3つのネストされたスコープがあります。最上位レベルのスコープ、`f()` のスコープ、`square()` のスコープです。観察事項

したがって、各スコープの環境は、`outer` というフィールドを介して周囲のスコープの環境を指します。変数の値を検索する場合、最初に現在の環境でその名前を検索し、次に外部環境で、次に外部環境の外部環境などで検索します。外部環境の全チェーンには、現在アクセスできるすべての変数が含まれています(シャドウイングされた変数を除く)。

関数呼び出しを行うと、新しい環境が作成されます。その環境の外部環境は、関数が作成された環境です。関数呼び出しによって作成された環境の `outer` フィールドを設定するのを助けるために、各関数は、その「誕生環境」を指す `[[Scope]]` という内部プロパティを持っています。

4.3.1 コードの実行

コードを実行している間にポーズする箇所を示します。

function f(x) {
  function square() {
    const result = x * x;
    // Pause 3
    return result;
  }
  // Pause 2
  return square();
}
// Pause 1
assert.equal(f(6), 36);

何が起こるかを示します。

図4:ネストされたスコープ、ポーズ1 – `f()` を呼び出す前:最上位レベルの環境には、`f()` の単一のエントリがあります。`f()` の誕生環境は最上位レベルの環境です。したがって、`f` の `[[Scope]]` はそれを指しています。
図5:ネストされたスコープ、ポーズ2 – `f()` を実行中:関数呼び出し `f(6)` の環境が作成されています。その環境の外部環境は、`f()` の誕生環境です(インデックス0の最上位レベルの環境)。`outer` フィールドが `f` の `[[Scope]]` の値に設定されていることがわかります。さらに、新しい関数 `square()` の `[[Scope]]` は、まさに作成された環境です。
図6:ネストされたスコープ、ポーズ3 – `square()` を実行中:前のパターンが繰り返されました。最新の環境の `outer` は、呼び出したばかりの関数の `[[Scope]]` を介して設定されました。`outer` を介して作成されたスコープのチェーンには、現在アクティブなすべての変数が含まれています。たとえば、必要であれば `result`、`square`、`f` にアクセスできます。環境は変数の2つの側面を反映しています。まず、外部環境のチェーンは、ネストされた静的スコープを反映しています。次に、実行コンテキストのスタックは、動的にどのような関数呼び出しが行われたかを反映しています。

4.4 クロージャと環境

クロージャ を実装するために環境がどのように使用されているかを確認するために、次の例を使用します。

function add(x) {
  return (y) => { // (A)
    return x + y;
  };
}
assert.equal(add(3)(1), 4); // (B)

ここで何が起こっているのでしょうか?`add()` は、関数を返す関数です。B行でネストされた関数呼び出し `add(3)(1)` を行うと、最初の引数は `add()` 用で、2番目の引数はそれが返す関数用です。これは、A行で作成された関数が、そのスコープを離れるときにその誕生スコープへの接続を失わないためです。関連付けられた環境はその接続によって存続し、関数はその環境内の変数 `x` にアクセスできます(`x` は関数内でフリーです)。

このネストされた `add()` の呼び出し方法には利点があります。最初の関数呼び出しのみを行う場合、パラメータ `x` がすでに記入されている `add()` のバージョンが得られます。

const plus2 = add(2);
assert.equal(plus2(5), 7);

2つのパラメータを持つ関数を、それぞれ1つのパラメータを持つ2つのネストされた関数に変換することを、カリー化といいます。`add()` はカリー化された関数です。

関数のいくつかのパラメータのみを入力することを、部分適用といいます(関数はまだ完全に適用されていません)。関数の `bind()` メソッド は部分適用を実行します。前の例では、関数がカリー化されている場合、部分適用が簡単であることがわかります。

4.4.0.1 コードの実行

次のコードを実行する際に、3つのポーズを行います。

function add(x) {
  return (y) => {
    // Pause 3: plus2(5)
    return x + y;
  }; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);

何が起こるかを示します。

図7:クロージャ、ポーズ1 – `add(2)` の実行中:`add()` によって返される関数がすでに存在し(右下隅を参照)、その内部プロパティ `[[Scope]]` を介してその誕生環境を指していることがわかります。`plus2` はまだテンポラルデッドゾーンにあり、初期化されていないことに注意してください。
図8:クロージャ、ポーズ2 – `add(2)` の実行後:`plus2` は現在、`add(2)` によって返された関数を指しています。その関数は、その `[[Scope]]` を介してその誕生環境(`add(2)` の環境)を存続させます。
図9:クロージャ、ポーズ3 – `plus2(5)` の実行中:`plus2` の `[[Scope]]` は、新しい環境の `outer` を設定するために使用されます。このようにして、現在の関数は `x` にアクセスできます。