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


ES2015 最大的特性之一就是有了一個全新的做用域。在這個章節裏,咱們將開始學習什麼是做用域。咱們將繼續學習如何建立新的做用域類型,以及給咱們代碼帶來的好處javascript

快速瞭解做用域

做用域描述爲一個變量,函數,標識符能夠被訪問的區域。JavaScript 傳統上有兩種做用域類型:全局做用域和函數做用域,你定義變量的位置會影響其餘代碼是否能夠訪問。讓咱們來看一個簡單的例子來闡述做用域的概念。想象一下,你的 JavaScript 文件只包含如下代碼:java

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 文件git

在全局做用域裏,咱們定義了兩個函數,globalFunction1globalFunction2,就像全局變量同樣,他們是 「可見的」 而且能夠在這個文件的任何地方調用,也能夠被同一頁面的其餘 JavaScript 文件調用。然而,當 JavaScript 引擎解析這些函數時,會分別建立他們本身的做用域。因吹斯聽,這兩個新的函數做用域被嵌套在全局做用域下,成爲子做用域。這也就意味着函數內的代碼能夠訪問全局變量,就像是和在函數 「內部的」 定義變量同樣github

當咱們試圖訪問 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 文件由譯者手動整理,若有勘誤,歡迎指正
  • 譯文和原文采用同樣協議,侵刪
相關文章
相關標籤/搜索