【譯】深入理解 ES2015,第一趴:塊作用域 let 和 const

Yangfan發表於2019-01-14

ES2015 最大的特性之一就是有了一個全新的作用域。在這個章節裡,我們將開始學習什麼是作用域。我們將繼續學習如何建立新的作用域型別,以及給我們程式碼帶來的好處

快速瞭解作用域

作用域描述為一個變數,函式,識別符號可以被訪問的區域。JavaScript 傳統上有兩種作用域型別:全域性作用域和函式作用域,你定義變數的位置會影響其他程式碼是否可以訪問。讓我們來看一個簡單的例子來闡述作用域的概念。想象一下,你的 JavaScript 檔案只包含以下程式碼:

var globalVariable = 'This is global';

function globalFunction1() {
  var innerVariable1 = 'Non-global variable 1';
}

function globalFunction2() {
  var innerVariable2 = 'Non-global variable 2';
}
複製程式碼

在上面的程式碼中,我們首先宣告瞭一個變數 globalVariable。這個語句不在函式內部,所以會自動存到全域性作用域中。瀏覽器用 window 物件建立了一個全域性作用域,除了可以用 globalVariable 訪問,我們還可以通過掛在 window 物件上的 window.globalVariable 訪問。我們可以在檔案的任何地方訪問這個變數,這兩個函式的之前或之後,甚至是在函式的內部(這就是為什麼我們說全域性變數是 “隱藏的”,我們可以在任何地方正確的訪問他們),甚至是在附在同一頁面的其他 JavaScript 檔案

在全域性作用域裡,我們定義了兩個函式,globalFunction1globalFunction2,就像全域性變數一樣,他們是 “可見的” 並且可以在這個檔案的任何地方呼叫,也可以被同一頁面的其他 JavaScript 檔案呼叫。然而,當 JavaScript 引擎解析這些函式時,會分別建立他們自己的作用域。因吹斯聽,這兩個新的函式作用域被巢狀在全域性作用域下,成為子作用域。這也就意味著函式內的程式碼可以訪問全域性變數,就像是和在函式 “內部的” 定義變數一樣

當我們試圖訪問 JavaScript 裡的識別符號時,瀏覽器會首先在當前作用域中查詢。如果沒有找到,瀏覽器會在當前作用域的父作用域中查詢,並且繼續向上查詢,直到找到這個變數,或者到達全域性作用域為止。如果這個變數在全域性作用域裡依舊沒有找到的話,那麼瀏覽器會丟擲一個 ReferenceError 錯誤。這種巢狀的作用域被稱作作用域鏈,而這個檢查當前作用域和父作用域的過程被稱作變數查詢。這種查詢只會向上查詢作用域鏈,它永遠不會在它的子作用域裡查詢

在上面的作用域鏈查詢方向我們得知,例子中的 innerVariable1 變數只能在 globalFunction1 函式內部被訪問,innerVariable2 變數只能在 globalFunction2 函式內部被訪問。innerVariable1 變數不能在 globalFunction2 函式內部或全域性作用域內被訪問,innerVariable2 變數也不能在 globalFunction1 函式內部或全域性作用域內被訪問

下面的圖片是上面程式碼中作用域的抽象表示:

js-scopes

全域性作用域包含了 globalVariable 以及兩個內嵌的函式作用域。每個內嵌的函式作用域又包含自己的變數,但是這些變數不能被全域性作用域訪問。虛線表示的是作用域鏈的查詢方向

讓我們來看下另一個簡短的程式碼示例,徹底的瞭解下到目前為止我們所介紹到的作用域概念。假設 JavaScript 檔案只包含如下程式碼:

function outer() {
  var variable1;

  function inner() {
    var variable2;
  }
}
複製程式碼

在這段程式碼裡,我們在全域性作用域裡宣告瞭一個叫 outer 的函式。因為它是一個函式,所以它建立了一個函式作用域,巢狀在全域性作用域下。在這個作用域下,我們又宣告瞭一個叫 variable1 的變數和 一個叫 inner 的函式。因為 inner 也是一個函式,所以一個新的作用域又被建立了,巢狀在 outer 函式的作用域下

inner 函式中,我們既可以訪問 variable2 也可以訪問 variable1。當我們在 inner 函式中訪問 variable1 時,瀏覽器首先會在它的作用域裡查詢這個變數;當這個變數沒有被找到時,會繼續向上在父作用域裡查詢(也就是 outer 函式的作用域)。程式碼裡作用域如下圖所示:

js-scopes2

函式作用域可以巢狀在其他的函式作用域裡,但是作用域鏈查詢規則是一樣的,因此在 inner 作用域下可以訪問到 variable1variable2,但是在 outer 作用域下只能訪問 variable1

這個示例中的作用域鏈比較長,從 inner 函式延伸到 outer 函式,直到全域性物件 window

JavaScript 的新作用域

在 JavaScript 中,一個塊是由一個或多個語句用大括號包裹起來的。諸如 ifforwhile 的條件表示式,都是用塊基於特定的條件來執行塊語句

其他流行的常見的程式語言都有塊作用域,JavaScript 作用域中,直到如今卻只有全域性作用域和函式作用域,因此使我們變得很困惑。ES2015 在 JavaScript 新增了塊作用域,對於我們的程式碼來說有很大的影響,並且對於那些熟悉其他程式語言的開發者來說變得更直觀

塊作用域意味著一個塊可以建立它自己的作用域,而不是簡單的存在於它最近到父級函式作用域或全域性作用域下。讓我們在認識塊作用域是如何工作的之前,先來了解下傳統上塊裡的 JavaScript 是如何工作的:

function fn() {
  var x = 'function scope';

  if (true) {
    var y = 'not block scope';
  }

  function innerFn() {
    console.log(x, y); // function scope not block scope
  }
  innerFn();
}
複製程式碼

var 語句是不能夠建立塊作用域的,即使是在塊裡,因此 console.log 語句可以訪問到 xy 變數。 fn 函式建立了一個函式作用域而且 xy 變數都是可以通過作用域內的作用域鏈訪問到

宣告提升

理解提升的概念是理解 JavaScript 如何工作的基礎。JavaScript 有兩個階段:解析階段(JavaScript 引擎讀取所有的程式碼)、執行階段(執行已解析的程式碼)。大多數的事情都發生在第二階段;例如,當你使用 console.log 語句時,實際的日誌訊息會在執行階段列印到控制檯

然而,一些重要的事情也會在解析階段發生,包括變數的記憶體分配、作用域建立。提升這個術語指的是 JavaScript 引擎在遇到識別符號,如變數、函式宣告時所發生到事情;當發生宣告提升時,它的行為就像是把它定義的字面量提升到當前作用域的頂部。鑑於此,上面到程式碼示例實際會變成如下情況:

function fn() {
  var x;
  var y;

  x = 'function scope';

  if (true) {
    y = 'not block scope';
  }

  function innerFn() {
    console.log(x, y); // function scope not block scope
  }
  innerFn();
}
複製程式碼

只有變數到宣告會提升到它的作用域的頂部;在這個例子的 if 語句中,變數賦值依然發生在我們所賦值的地方。當然,我們到變數並不會移動,而是引擎行為表現如此,因此這樣可以更好的幫助我們理解程式碼

除了變數,函式宣告也會被提升。結果就是,從 JavaScript 引擎到角度來看,程式碼實際上看起來是這樣的:

function fn() {
  var x;
  var y;
  function innerFn() {
    console.log(x, y); // function scope not block scope
  }

  x = 'function scope';

  if (true) {
    y = 'not block scope';
  }
  innerFn();
}
複製程式碼

innerFn 的宣告也被提升到了它的作用域的頂部。但是,記住它僅僅是函式宣告被提升了,函式呼叫沒有被提升。上面的程式碼並不會報任何錯,因為 innerFnxy 賦值之前並沒有被呼叫

使用 let

即使使用了 ES2015,var 宣告也不會建立塊作用域。為了建立塊作用域,我們需要在塊裡使用 letconst 宣告。我們一會再看 const,首先來看下 let

表面上,letvar(我們用它來宣告變數)的行為很相似:

function fn() {
  var variable1;
  let variable2;
}
複製程式碼

在這個簡單的例子中,varlet 宣告都做了相同的事情(在 fn 建立的作用域下初始化了一個新的變數)。為了建立一個新的塊作用域,我們需要在塊裡使用 let

function fn() {
  var variable1 = 'function scope';

  if (true) {
    let variable2 = 'block scope';
  }

  console.log(variable1, variable2); // Uncaught ReferenceError: variable2 is not defined
}
fn();
複製程式碼

在這個程式碼示例中,丟擲了一個引用錯誤(reference error);讓我們來探索下為什麼會這樣。fn 函式建立了一個新作用域,裡面宣告瞭變數 variable1。然後我們在 if 語句的塊裡,宣告瞭變數 variable2。然而,因為我們在塊裡使用了 let 宣告,因此一個新的塊作用域在 fn 的作用域下被建立了

如果 console.log 語句也在 if 塊中的話,那麼它就和 variable2 在相同的作用域下了,也能夠通過作用域鏈找到 variable1。但是因為 console.log 在外頭,因此它不能訪問 variable2,所以會丟擲一個引用錯誤

塊作用域和函式作用域的行為相同,但是他們是為塊建立的,而不是函式

暫時性死區

當一個用 var 宣告的常規變數被建立時,會被提升到它的作用域的頂部,然後並初始化一個 undefined 值,這樣就允許我們能夠在它賦值之前引用一個常規變數

console.log(x); // undefined
var x = 10;
複製程式碼

記住,由於存在宣告提升,程式碼實際看起來是這樣的:

var x = undefined;
console.log(x); // undefined
x = 10;
複製程式碼

這個行為會阻止丟擲引用錯誤 ReferenceError

let 宣告的變數也被提升了,但重要的是,他們並不會自動初始化值 undefined,因此意味著下面的程式碼會產生一個錯誤:

console.log(x); // Uncaught ReferenceError: x is not defined
let x = 10;
複製程式碼

這個錯誤是由暫時性死區(TDZ)引起的。TDZ 存在於作用域初始化到變數宣告期間。為了修復這個錯誤(ReferenceError),我們需要在訪問它前宣告它:

譯者注:TDZ

let x;
console.log(x); // undefined
x = 10;
複製程式碼

TDZ 這樣設計是為了使開發更容易(試圖引用一個還沒宣告的變數通常視為一個錯誤,而不是故意為之),因此這個錯誤可以立即提醒我們

使用 const

新的 const 被用來宣告一個不可再次賦值的變數。它和 let 的在 TDZ 的行為非常相似,但是,const 變數必須初始化一個值

const VAR1 = 'constant';
複製程式碼

從現在開始, 變數 VAR1 的值將永遠是 “constant” 這個字串。如果我們試圖再次對它賦值,我們會得到一個錯誤:

TypeError: Assignment to constant variable

如果我們試圖建立一個沒有初始化的 const 變數,我們將看到一個語法錯誤:

SyntaxError: Missing initializer in const declaration

相似地,一個 const 變數不能被再次宣告。如果我們試圖再次用 const 宣告一個相同變數時,我們將得到一個不同型別的語法錯誤

SyntaxError: Identifier ‘VAR1′ has already been declared

和其他程式語言一樣,常量是被用來儲存我們的程式在生命週期裡不希望改變的值

記住 letconst 都是 JavaScript 的保留詞,因此在嚴格模式下,是不能被用作識別符號名稱的(變數名,函式名等)。隨著 ES2015 越來越普遍,letconst 優於 var 已形成一個共識,因為變數建立的作用域更與其他現代程式語言看齊,並且程式碼的行為也更好預測。 因此,在大多數情況下儘可能的避免使用 var

不可變性

const 宣告的變數不能被再次賦值的,但是 const 宣告的變數並不是完全不可變的。如果我們用物件或陣列初始化了一個 const 變數,我們依然可以修改物件的屬性和增加刪除陣列的元素

練習

  1. for 迴圈裡用 let 來初始化計數器變數
  2. 修復下面 const 的錯誤:
const VAR1 = 'constant';
const VAR1 = 'constant2';
const VAR2;
VAR2 = 'constant';
複製程式碼

成功是通過不斷的練習和知識的積累,而非智力


  • 本文僅代表原作者個人觀點,譯者不發表任何觀點
  • Markdown 檔案由譯者手動整理,如有勘誤,歡迎指正
  • 譯文和原文采用一樣協議,侵刪

相關文章