原文連接:Variable Scope in Modern JavaScriptjavascript
譯者:OFEDhtml
當與其餘 JavaScript 開發人員交談時,令我常常感到驚訝的是,有不少人不知道變量做用域是如何在 JavaScript 裏起做用的。這裏咱們說的做用域指的是代碼裏變量的可見性;或者換句話說,哪部分代碼能夠訪問和修改變量。我發現你們在代碼中常常用 var
聲明變量,而並不知道 JavaScript 將如何處理這些變量。java
過去幾年中,JavaScript 經歷了一些巨大的變化;這些變化包括新的變量聲明關鍵字以及新的做用域處理方式。ES6(ES2015) 新增了 let
和 const
命令,至今已經有三年時間,瀏覽器支持很好,對於其餘新增特性,可使用 Babel 將 ES6 轉換成普遍支持的 JavaScript。如今是時候回顧下如何聲明變量,以及增長對做用域的瞭解了。node
在這篇博文中,我將經過大量 JavaScript 示例來展現全局、局部和塊級做用域是如何工做的。咱們還將爲那些仍不熟悉這部份內容的人演示如何使用 let
和 const
來聲明變量。git
讓咱們從全局做用域提及。全局定義的變量在代碼中任何地方均可以訪問和修改(幾乎均可以,可是咱們稍後會提到例外的狀況)。github
聲明在任何函數以外的頂層做用域的變量就是全局變量。web
var a = 'Fred Flinstone'; // 全局變量
function alpha() {
console.log(a);
}
alpha(); // 輸出 'Fred Flinstone'
複製代碼
在這個例子中,a
是一個全局變量;所以,在任何函數中都能被輕鬆獲取。所以,咱們能夠從方法 alpha
輸出 a
的值。當咱們調用 alpha
方法時,控制檯輸出 Fred Flinstone
。面試
在 web 瀏覽器聲明全局變量時,它會做爲全局 window
對象的屬性。看看這個例子:api
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'
複製代碼
使用全局變量要當心。它們會致使代碼的可讀性變差,同時變得很難測試。我已經看到許多開發人員在查找變量值什麼時候被重置時遇到了意想不到的問題。將變量做爲參數傳遞給函數要比依賴全局變量好得多。全局變量應該儘可能少用。
若是你確實須要使用全局變量,最好定義命名空間,使它們成爲全局對象的屬性。例如,建立一個名爲 globals
或 app
的全局對象。
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 裏是一個全局變量
複製代碼
須要注意的是,若是沒有使用關鍵字 var
、let
或 const
之一來聲明變量,那麼變量屬於全局做用域。
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。還好 let
和 const
的出現解救了咱們。那麼讓咱們看看如何使用 let
建立塊級做用域。
幾年前隨着 ES6 的到來,出現了兩個用於聲明變量的新關鍵詞: let
和 const
。這兩個關鍵字都容許咱們將做用域擴大到代碼塊,即介於兩個大括號{ }之間的內容。
許多人認爲 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'
複製代碼
在本例中,b
在 for
循環範圍內的塊級做用域(其中包括條件塊)內起做用。所以,它將輸出奇數 1 和 3 ,而後拋出一個錯誤,由於咱們不能在它的做用域以外訪問 b
。
咱們以前看到 JavaScript 奇怪的變量提高而影響到函數 zeta
的結果,若是咱們重寫函數使用let會發生什麼呢?
var d = 'Tom';
function zeta() {
if (d === undefined) {
let d = 'Jerry';
}
console.log(d);
}
zeta();
複製代碼
這一次 zeta
輸出 「Tom」 ,由於 d 被限定爲做用在條件塊內,可是這是否意味着這裏沒有提高?不,當咱們使用 let
或 const
時, JavaScript 仍然會將變量提高到做用域的頂部,可是和 var
不一樣的是,var
聲明的變量提高後初始值爲 undefined
,let
和 const
聲明的變量提高後沒有初始化,它們存在於暫時性死區中。
讓咱們看一下在初始化聲明以前使用一個塊級做用域的變量會發生什麼。
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
。這個關鍵字與 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();
複製代碼
我但願此篇做用域的總結能讓你們更好的理解 JavaScript 如何處理變量。貫穿全文的示例中我使用 var,let 和 const 定義變量。伴隨着 ES6 的降臨,咱們大可使用 let 和 const 取代 var。
那麼 var 多餘了嗎?沒有真正對錯的答案,可是我的而言,我依舊習慣用 var 定義頂層的全局變量。但是,我會保守地使用全局變量,而是用全局命名空間代替。此外,再也不改變的變量我用 const,剩餘的其餘狀況我用 let。
最終你會如何定義變量呢,仍是但願你能更好的理解代碼中的做用域範圍。