在本章中,我們將深入探討 ECMAScript 語言規範如何處理變數。
環境是 ECMAScript 規範用於管理變數的資料結構。它是一個字典,其鍵是變數名稱,其值是這些變數的值。每個範圍都有其關聯的環境。環境必須能夠支援與變數相關的下列現象
我們將使用範例來說明如何對每個現象執行此操作。
我們將首先處理遞迴。考慮以下程式碼
function f(x) {
return x * 2;
}
function g(y) {
const tmp = y + 1;
return f(tmp);
}
assert.equal(g(3), 8);
對於每個函式呼叫,您需要為呼叫函式的變數(參數和局部變數)建立新的儲存空間。這是透過所謂的執行內容堆疊來管理,這些內容是對環境的參考(就本章而言)。環境本身儲存在堆積區中。這是必要的,因為它們偶爾會在執行離開其範圍後繼續存在(我們將在探索封閉時看到)。因此,它們本身無法透過堆疊來管理。
在執行程式碼時,我們暫停以下事項
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 – 在呼叫 g()
之前(圖 1)。
暫停 2 – 在執行 g()
時(圖 2)。
暫停 3 – 在執行 f()
時(圖 3)。
剩餘步驟:每次有 return
時,一個執行內容就會從堆疊中移除。
g()
之前:執行內容堆疊有一個條目,指向頂層環境。在那個環境中,有兩個條目;一個是 f()
,另一個是 g()
。g()
時:執行內容堆疊的頂端指向為 g()
建立的環境。該環境包含參數 y
和局部變數 tmp
的條目。f()
時:頂層執行環境現在指向 f()
的環境。我們使用下列程式碼來探討如何透過環境實作巢狀作用域。
function f(x) {
function square() {
const result = x * x;
return result;
}
return square();
}
assert.equal(f(6), 36);
在此,我們有三個巢狀作用域:頂層作用域、f()
的作用域,以及 square()
的作用域。觀察
因此,每個作用域的環境透過稱為 outer
的欄位指向周圍作用域的環境。當我們查詢變數的值時,我們會先在目前環境中搜尋它的名稱,然後在外圍環境中搜尋,然後在外圍環境的外圍環境中搜尋,依此類推。整個外圍環境鏈包含目前可存取的所有變數(減去遮蔽的變數)。
當您呼叫函式時,您會建立一個新的環境。該環境的外圍環境是在其中建立函式的環境。為了協助設定透過函式呼叫建立的環境的 outer
欄位,每個函式都有稱為 [[Scope]]
的內部屬性,指向它的「誕生環境」。
這些是我們在執行程式碼時所做的暫停
function f(x) {
function square() {
const result = x * x;
// Pause 3
return result;
}
// Pause 2
return square();
}
// Pause 1
assert.equal(f(6), 36);
以下是發生的事情
f()
之前(圖 4)。f()
時(圖 5)。square()
時(圖 6)。return
陳述式將執行條目從堆疊中彈出。f()
之前:頂層環境有一個條目,用於 f()
。f()
的誕生環境是頂層環境。因此,f
的 [[Scope]]
指向它。f()
時:現在有一個環境用於函式呼叫 f(6)
。該環境的外圍環境是 f()
的誕生環境(索引 0 處的頂層環境)。我們可以看到 outer
欄位已設定為 f
的 [[Scope]]
的值。此外,新函式 square()
的 [[Scope]]
是剛才建立的環境。square()
時:先前的模式重複:最新環境的 outer
是透過我們剛呼叫的函式的 [[Scope]]
設定的。透過 outer
建立的作用域鏈,包含所有目前作用中的變數。例如,如果我們想存取 result
、square
和 f
,我們可以。環境反映變數的兩個面向。首先,外部環境的鏈反映巢狀靜態作用域。其次,執行內容的堆疊反映動態進行的函式呼叫。為了了解環境如何用於實作 封閉,我們使用以下範例
這裡發生了什麼事?add()
是傳回函式的函式。當我們在 B 行進行巢狀函式呼叫 add(3)(1)
時,第一個參數是給 add()
,第二個參數是給它傳回的函式。這之所以可行,是因為在 A 行建立的函式在離開作用域時,並不會失去與其建立作用域的連結。透過此連結,關聯的環境會持續存在,而函式仍可以在該環境中存取變數 x
(x
在函式內部是自由的)。
這種呼叫 add()
的巢狀方式有一個優點:如果你只進行第一次函式呼叫,你會取得一個 add()
的版本,其參數 x
已經填入
將具有兩個參數的函式轉換成兩個具有各一個參數的巢狀函式,稱為柯里化。add()
是柯里化函式。
只填入函式部分參數稱為部分應用(函式尚未完全應用)。函式的 方法 .bind()
執行部分應用。在先前的範例中,我們可以看到,如果函式是柯里化的,部分應用很簡單。
在執行以下程式碼時,我們暫停三次
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);
以下是發生的事情
add(2)
時:我們可以看到 add()
回傳的函式已經存在(請參閱右下角),而且它透過其內部屬性 [[Scope]]
指向其誕生環境。請注意,plus2
仍處於其暫時性死區,尚未初始化。add(2)
之後:plus2
現在指向 add(2)
回傳的函式。該函式透過其 [[Scope]]
保持其誕生環境(add(2)
的環境)存活。plus2(5)
時:plus2
的 [[Scope]]
用於設定新環境的 outer
。這就是目前函式如何取得 x
的存取權。