深入 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() 之前:執行內容堆疊有一個條目,指向頂層環境。在那個環境中,有兩個條目;一個是 f(),另一個是 g()
圖 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);

在此,我們有三個巢狀作用域:頂層作用域、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 建立的作用域鏈,包含所有目前作用中的變數。例如,如果我們想存取 resultsquaref,我們可以。環境反映變數的兩個面向。首先,外部環境的鏈反映巢狀靜態作用域。其次,執行內容的堆疊反映動態進行的函式呼叫。

4.4 封閉和環境

為了了解環境如何用於實作 封閉,我們使用以下範例

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

這裡發生了什麼事?add() 是傳回函式的函式。當我們在 B 行進行巢狀函式呼叫 add(3)(1) 時,第一個參數是給 add(),第二個參數是給它傳回的函式。這之所以可行,是因為在 A 行建立的函式在離開作用域時,並不會失去與其建立作用域的連結。透過此連結,關聯的環境會持續存在,而函式仍可以在該環境中存取變數 xx 在函式內部是自由的)。

這種呼叫 add() 的巢狀方式有一個優點:如果你只進行第一次函式呼叫,你會取得一個 add() 的版本,其參數 x 已經填入

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

將具有兩個參數的函式轉換成兩個具有各一個參數的巢狀函式,稱為柯里化add() 是柯里化函式。

只填入函式部分參數稱為部分應用(函式尚未完全應用)。函式的 方法 .bind() 執行部分應用。在先前的範例中,我們可以看到,如果函式是柯里化的,部分應用很簡單。

4.4.0.1 執行程式碼

在執行以下程式碼時,我們暫停三次

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 的存取權。