現代 JavaScript 的變數作用域

塗鴉碼龍發表於2018-08-17

原文連結:Variable Scope in Modern JavaScript

譯者:OFED

現代 JavaScript 的變數作用域

當與其他 JavaScript 開發人員交談時,令我經常感到驚訝的是,有很多人不知道變數作用域是如何在 JavaScript 裡起作用的。這裡我們說的作用域指的是程式碼裡變數的可見性;或者換句話說,哪部分程式碼可以訪問和修改變數。我發現大家在程式碼中經常用 var 宣告變數,而並不知道 JavaScript 將如何處理這些變數。

過去幾年中,JavaScript 經歷了一些巨大的變化;這些變化包括新的變數宣告關鍵字以及新的作用域處理方式。ES6(ES2015) 新增了 letconst 命令,至今已經有三年時間,瀏覽器支援很好,對於其他新增特性,可以使用 Babel 將 ES6 轉換成廣泛支援的 JavaScript。現在是時候回顧下如何宣告變數,以及增加對作用域的瞭解了。

在這篇博文中,我將通過大量 JavaScript 示例來展示全域性區域性塊級作用域是如何工作的。我們還將為那些仍不熟悉這部分內容的人演示如何使用 letconst 來宣告變數。

全域性作用域

讓我們從全域性作用域說起。全域性定義的變數在程式碼中任何地方都可以訪問和修改(幾乎都可以,但是我們稍後會提到例外的情況)。

宣告在任何函式之外的頂層作用域的變數就是全域性變數。

var a = 'Fred Flinstone'; // 全域性變數
function alpha() {
    console.log(a);
}
alpha(); // 輸出 'Fred Flinstone'
複製程式碼

在這個例子中,a 是一個全域性變數;因此,在任何函式中都能被輕鬆獲取。因此,我們可以從方法 alpha 輸出 a 的值。當我們呼叫 alpha 方法時,控制檯輸出 Fred Flinstone

在 web 瀏覽器宣告全域性變數時,它會作為全域性 window 物件的屬性。看看這個例子:

var b = 'Wilma Flintstone';
window.b = 'Betty Rubble';
console.log(b); // 輸出 'Betty Rubble'
複製程式碼

b 可以作為 window 物件的屬性(window.b)被訪問/修改。當然,沒有必要通過 window 物件修改 b 的值,這只是為了證明這一點。我們更有可能將上述情況寫成下面的形式:

var b = 'Wilma Flintstone';
b = 'Betty Rubble';
console.log(b); // 輸出 'Betty Rubble'
複製程式碼

使用全域性變數要小心。它們會導致程式碼的可讀性變差,同時變得很難測試。我已經看到許多開發人員在查詢變數值何時被重置時遇到了意想不到的問題。將變數作為引數傳遞給函式要比依賴全域性變數好得多。全域性變數應該儘量少用。

如果你確實需要使用全域性變數,最好定義名稱空間,使它們成為全域性物件的屬性。例如,建立一個名為 globalsapp 的全域性物件。

var app = {}; // 全域性物件
app.foo = 'Homer';
app.bar = 'Marge';
function beta() {
    console.log(app.bar);
}
beta(); // 輸出 'Marge'
複製程式碼

如果你正在使用 NodeJS,則頂層作用域與全域性作用域不同。如果在 NodeJS 模組中使用 var foobar ,則它是該模組的區域性變數。要在 NodeJS 中定義全域性變數,我們需要使用全域性名稱空間物件global

global.foobar = 'Hello World!'; // 在 NodeJS 裡是一個全域性變數
複製程式碼

需要注意的是,如果沒有使用關鍵字 varletconst 之一來宣告變數,那麼變數屬於全域性作用域。

function gamma() {
    c = 'Top Cat';
}
gamma();
console.log(c); // 輸出 'Top Cat'
console.log(window.c); // 輸出 'Top Cat'
複製程式碼

我們推薦始終使用一種變數關鍵字定義變數。這樣,程式碼中的每一個變數作用域是可控的。正如以上例子,希望你能意識到不用關鍵字的潛在危險。

區域性作用域

現在我們回到區域性作用域

var a = 'Daffy Duck'; // a 是全域性變數
function delta(b) {
  // b 是傳入 delta 的區域性變數
  console.log(b);
}
function epsilon() {
  // c 被定義成區域性作用域變數
  var c = 'Bugs Bunny';
  console.log(c);
}
delta(a); // 輸出 'Daffy Duck'
epsilon(); // 輸出 'Bugs Bunny'
console.log(b); // 丟擲錯誤:b 在全域性作用域未定義
複製程式碼

在函式內部定義的變數,作用域限制在函式內。以上例子中,b 和 c 對於各自的函式而言是區域性的。可是出現以下的寫法,輸出結果會是什麼呢?

var d = 'Tom';
function zeta() {
  if (d === undefined) {
    var d = 'Jerry';
  }
  console.log(d);
}
zeta();
複製程式碼

答案是 'Jerry',這可能是常考的面試題之一。zeta 函式內部定義了一個新的區域性變數 d,當用 var 定義變數的時候,JavaScript 會在當前作用域的頂部初始化它,不管它在程式碼的哪一部分。

var d = 'Tom';
function zeta() {
  var d;
  if (d === undefined) {
    d = 'Jerry';
  }
  console.log(d);
}
zeta();
複製程式碼

這被稱之為提升,它是 JavaScript 的特性之一,而且需要注意的是,沒在作用域的頂部初始化變數,容易引起一些 bug。還好 letconst 的出現解救了我們。那麼讓我們看看如何使用 let 建立塊級作用域。

塊級作用域

幾年前隨著 ES6 的到來,出現了兩個用於宣告變數的新關鍵詞: letconst。這兩個關鍵字都允許我們將作用域擴大到程式碼塊,即介於兩個大括號{ }之間的內容。

let

許多人認為 let 是對現有 var 的替代。然而,這並不完全正確,因為它們宣告變數的作用域不同。let 宣告的是塊級作用域的變數,然而var 語句允許我們建立區域性作用域的變數。當然,函式內我們可以使用 let 宣告塊級作用域,就像我們以前使用 var 一樣。

function eta() {
    let a = 'Scooby Doo';
}
eta();
複製程式碼

這裡 a 的作用域為函式 eta 內。我們還可以擴充套件到條件塊和迴圈。塊級作用域包括變數定義的頂層塊中包含的任何子塊。

for (let b = 0; b < 5; b++) {
    if (b % 2) {
        console.log(b);
    }
}
console.log(b); // 'ReferenceError: b is not defined'
複製程式碼

在本例中,bfor 迴圈範圍內的塊級作用域(其中包括條件塊)內起作用。因此,它將輸出奇數 1 和 3 ,然後丟擲一個錯誤,因為我們不能在它的作用域之外訪問 b

我們之前看到 JavaScript 奇怪的變數提升而影響到函式 zeta 的結果,如果我們重寫函式使用let會發生什麼呢?

var d = 'Tom';
function zeta() {
    if (d === undefined) {
        let d = 'Jerry';
    }
    console.log(d);
}
zeta();
複製程式碼

這一次 zeta 輸出 “Tom” ,因為 d 被限定為作用在條件塊內,但是這是否意味著這裡沒有提升?不,當我們使用 letconst 時, JavaScript 仍然會將變數提升到作用域的頂部,但是和 var 不同的是,var宣告的變數提升後初始值為 undefinedletconst 宣告的變數提升後沒有初始化,它們存在於暫時性死區中。

讓我們看一下在初始化宣告之前使用一個塊級作用域的變數會發生什麼。

function theta() {
    console.log(e); // 輸出 'undefined'
    console.log(f); // 'ReferenceError: d is not defined'
    var e = 'Wile E. Coyote';
    let f = 'Road Runner';
}
theta();
複製程式碼

因此,呼叫 theta 將為區域性作用域的變數 e 輸出 undefined,併為塊級作用域的變數 f 丟擲一個錯誤。在啟動 f 之前,我們不能使用它,在這種情況下,我們將其值設定為 “Road Runner”。

在繼續之前我們需要說明一下,在let和var之間還有一個重要的區別。當我們在程式碼的最頂層使用var時,它會變成一個全域性變數,並在瀏覽器中新增到window物件中。使用let,雖然變數將變為全域性變數,因為它的作用域是整個程式碼庫的塊,但它不會成為window物件的屬性。

var g = 'Pinky';
let h = 'The Brain';
console.log(window.g); // 輸出 'Pinky'
console.log(window.h); // 輸出 undefined
複製程式碼

const

我之前順便提到過 const。這個關鍵字與 let 一起作為 ES6 的一部分引入。就作用域而言,它與 let 的工作原理相同。

if (true) {
  const a = 'Count Duckula';
  console.log(a); // 輸出 'Count Duckula'
}
console.log(a); // 輸出 'ReferenceError: a is not defined'
複製程式碼

在本例中,a 的作用域是 if 語句,因此可以在條件語句內部訪問,但在條件語句外部是 undefined

let 不同,const 定義的變數不能通過重新賦值來改變。

const b = 'Danger Mouse';
b = 'Greenback'; // 丟擲 'TypeError: Assignment to constant variable'
複製程式碼

然而,當使用陣列或物件時,情況有點不同。我們仍然無法重新賦值,因此以下操作將失敗

const c = ['Sylvester', 'Tweety'];
c = ['Tom', 'Jerry']; // 丟擲 'TypeError: Assignment to constant variable'
複製程式碼

但是,我們可以修改常量陣列或物件,除非我們在變數上使用 Object.freeze() 使其不可變。

const d = ['Dick Dastardly', 'Muttley'];
d.pop();
d.push('Penelope Pitstop');
Object.freeze(d);
console.log(d); // 輸出 ["Dick Dastardly", "Penelope Pitstop"]
d.push('Professor Pat Pending'); // 丟擲錯誤
複製程式碼

全域性 + 區域性作用域

當我們在區域性作用域重新定義已經存在的全域性變數時會發生什麼呢。

var a = 'Johnny Bravo'; // 全域性作用域
function iota() {
  var a = 'Momma'; // 區域性作用域
  console.log(a); // 輸出 'Momma'
  console.log(window.a); // 輸出 'Johnny Bravo'
}
iota();
console.log(a); // 輸出 'Johnny Bravo'
複製程式碼

當我們在區域性作用域重定義全域性變數的時候,JavaScript 初始化了一個新的區域性變數。例子中,已有一個全域性變數 a,函式 iota 內部又建立了一個新的區域性變數 a。新的區域性變數並沒有修改全域性變數,如果我們想在函式內部訪問全域性變數的值,需要使用全域性的 window 物件。

對我而言,以下程式碼更易讀,使用全域性名稱空間代替了全域性變數,使用塊級作用域重寫了我們的函式:-

var globals = {};
globals.a = 'Johnny Bravo'; // 全域性作用域
function iota() {
    let a = 'Momma'; // 區域性作用域
    console.log(a); // 輸出 'Momma'
    console.log(globals.a); // 輸出 'Johnny Bravo'
}
iota();
console.log(globals.a); // 輸出 'Johnny Bravo'
複製程式碼

區域性 + 塊級作用域

希望以下的程式碼如你所願。

function kappa() {
    var a = 'Him'; // 區域性作用域
    if (true) {
        let a = 'Mojo Jojo'; // 塊級作用域
        console.log(a); // 輸出 'Mojo Jojo'
    }
    console.log(a); // 輸出 'Him'
}
kappa();
複製程式碼

以上程式碼並不是特別易讀,但是塊級作用域變數只能在定義的塊級內訪問。在塊級作用域外面修改塊級變數毫無效果,用 let 重定義變數 a,同樣沒效果,如下例:

function kappa() {
    let a = 'Him';
    if (true) {
        let a = 'Mojo Jojo';
        console.log(a); // 輸出 'Mojo Jojo'
    }
    console.log(a); // 輸出 'Him'
}
kappa();
複製程式碼

var,let 還是 const?

我希望此篇作用域的總結能讓大家更好的理解 JavaScript 如何處理變數。貫穿全文的示例中我使用 var,let 和 const 定義變數。伴隨著 ES6 的降臨,我們大可以使用 let 和 const 取代 var。

那麼 var 多餘了嗎?沒有真正對錯的答案,但是個人而言,我依舊習慣用 var 定義頂層的全域性變數。可是,我會保守地使用全域性變數,而是用全域性名稱空間代替。此外,不再改變的變數我用 const,剩餘的其他情況我用 let。

最終你會如何定義變數呢,還是希望你能更好的理解程式碼中的作用域範圍。

相關文章