es6新增了<code>let</code>指令,用來聲明變量。它的用法類似于<code>var</code>,但是所聲明的變量,隻在<code>let</code>指令所在的代碼塊内有效。
上面代碼在代碼塊之中,分别用<code>let</code>和<code>var</code>聲明了兩個變量。然後在代碼塊之外調用這兩個變量,結果<code>let</code>聲明的變量報錯,<code>var</code>聲明的變量傳回了正确的值。這表明,<code>let</code>聲明的變量隻在它所在的代碼塊有效。
<code>for</code>循環的計數器,就很合适使用let指令。
上面代碼的計數器<code>i</code>,隻在<code>for</code>循環體内有效。
下面的代碼如果使用<code>var</code>,最後輸出的是10。
上面代碼中,變量<code>i</code>是<code>var</code>聲明的,在全局範圍内都有效。是以每一次循環,新的<code>i</code>值都會覆寫舊值,導緻最後輸出的是最後一輪的<code>i</code>的值。
如果使用<code>let</code>,聲明的變量僅在塊級作用域内有效,最後輸出的是6。
上面代碼中,變量<code>i</code>是<code>let</code>聲明的,目前的<code>i</code>隻在本輪循環有效,是以每一次循環的<code>i</code>其實都是一個新的變量,是以最後輸出的是6。
<code>let</code>不像<code>var</code>那樣會發生“變量提升”現象。是以,變量一定要在聲明後使用,否則報錯。
上面代碼中,變量<code>foo</code>用<code>var</code>指令聲明,會發生變量提升,即腳本開始運作時,變量<code>foo</code>已經存在了,但是沒有值,是以會輸出<code>undefined</code>。變量<code>bar</code>用<code>let</code>指令聲明,不會發生變量提升。這表示在聲明它之前,變量<code>bar</code>是不存在的,這時如果用到它,就會抛出一個錯誤。
隻要塊級作用域記憶體在<code>let</code>指令,它所聲明的變量就“綁定”(binding)這個區域,不再受外部的影響。
上面代碼中,存在全局變量<code>tmp</code>,但是塊級作用域内<code>let</code>又聲明了一個局部變量<code>tmp</code>,導緻後者綁定這個塊級作用域,是以在<code>let</code>聲明變量前,對<code>tmp</code>指派會報錯。
es6明确規定,如果區塊中存在<code>let</code>和<code>const</code>指令,這個區塊對這些指令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
總之,在代碼塊内,使用let指令聲明變量之前,該變量都是不可用的。這在文法上,稱為“暫時性死區”(temporal dead zone,簡稱tdz)。
上面代碼中,在<code>let</code>指令聲明變量<code>tmp</code>之前,都屬于變量<code>tmp</code>的“死區”。
“暫時性死區”也意味着<code>typeof</code>不再是一個百分之百安全的操作。
上面代碼中,變量<code>x</code>使用<code>let</code>指令聲明,是以在聲明之前,都屬于<code>x</code>的“死區”,隻要用到該變量就會報錯。是以,<code>typeof</code>運作時就會抛出一個<code>referenceerror</code>。
作為比較,如果一個變量根本沒有被聲明,使用<code>typeof</code>反而不會報錯。
上面代碼中,<code>undeclared_variable</code>是一個不存在的變量名,結果傳回“undefined”。是以,在沒有<code>let</code>之前,<code>typeof</code>運算符是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的程式設計習慣,變量一定要在聲明之後使用,否則就報錯。
有些“死區”比較隐蔽,不太容易發現。
上面代碼中,調用<code>bar</code>函數之是以報錯(某些實作可能不報錯),是因為參數<code>x</code>預設值等于另一個參數<code>y</code>,而此時<code>y</code>還沒有聲明,屬于”死區“。如果<code>y</code>的預設值是<code>x</code>,就不會報錯,因為此時<code>x</code>已經聲明了。
es6規定暫時性死區和<code>let</code>、<code>const</code>語句不出現變量提升,主要是為了減少運作時錯誤,防止在變量聲明前就使用這個變量,進而導緻意料之外的行為。這樣的錯誤在es5是很常見的,現在有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,隻要一進入目前作用域,所要使用的變量就已經存在了,但是不可擷取,隻有等到聲明變量的那一行代碼出現,才可以擷取和使用該變量。
let不允許在相同作用域内,重複聲明同一個變量。
是以,不能在函數内部重新聲明參數。
es5隻有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。
第一種場景,内層變量可能會覆寫外層變量。
上面代碼中,函數f執行後,輸出結果為<code>undefined</code>,原因在于變量提升,導緻内層的tmp變量覆寫了外層的tmp變量。
第二種場景,用來計數的循環變量洩露為全局變量。
上面代碼中,變量i隻用來控制循環,但是循環結束後,它并沒有消失,洩露成了全局變量。
<code>let</code>實際上為javascript新增了塊級作用域。
上面的函數有兩個代碼塊,都聲明了變量<code>n</code>,運作後輸出5。這表示外層代碼塊不受内層代碼塊的影響。如果使用<code>var</code>定義變量<code>n</code>,最後輸出的值就是10。
es6允許塊級作用域的任意嵌套。
上面代碼使用了一個五層的塊級作用域。外層作用域無法讀取内層作用域的變量。
内層作用域可以定義外層作用域的同名變量。
塊級作用域的出現,實際上使得獲得廣泛應用的立即執行匿名函數(iife)不再必要了。
函數能不能在塊級作用域之中聲明,是一個相當令人混淆的問題。
es5規定,函數隻能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。
上面代碼的兩種函數聲明,根據es5的規定都是非法的。
但是,浏覽器沒有遵守這個規定,還是支援在塊級作用域之中聲明函數,是以上面兩種情況實際都能運作,不會報錯。不過,“嚴格模式”下還是會報錯。
es6引入了塊級作用域,明确允許在塊級作用域之中聲明函數。
并且es6規定,塊級作用域之中,函數聲明語句的行為類似于<code>let</code>,在塊級作用域之外不可引用。
上面代碼在es5中運作,會得到“i am inside!”,因為在<code>if</code>内聲明的函數<code>f</code>會被提升到函數頭部,實際運作的代碼如下。
es6的運作結果就完全不一樣了,會得到“i am outside!”。因為塊級作用域内聲明的函數類似于<code>let</code>,對作用域之外沒有影響,實際運作的代碼如下。
允許在塊級作用域内聲明函數。
函數聲明類似于<code>var</code>,即會提升到全局作用域或函數作用域的頭部。
同時,函數聲明還會提升到所在的塊級作用域的頭部。
注意,上面三條規則隻對es6的浏覽器實作有效,其他環境的實作不用遵守,還是将塊級作用域的函數聲明當作<code>let</code>處理。
前面那段代碼,在chrome環境下運作會報錯。
上面的代碼報錯,是因為實際運作的是下面的代碼。
考慮到環境導緻的行為差異太大,應該避免在塊級作用域内聲明函數。如果确實需要,也應該寫成函數表達式,而不是函數聲明語句。
另外,還有一個需要注意的地方。es6的塊級作用域允許聲明函數的規則,隻在使用大括号的情況下成立,如果沒有使用大括号,就會報錯。
<code>const</code>聲明一個隻讀的常量。一旦聲明,常量的值就不能改變。
上面代碼表明改變常量的值會報錯。
<code>const</code>聲明的變量不得改變值,這意味着,const一旦聲明變量,就必須立即初始化,不能留到以後指派。
上面代碼表示,對于<code>const</code>來說,隻聲明不指派,就會報錯。
<code>const</code>的作用域與<code>let</code>指令相同:隻在聲明所在的塊級作用域内有效。
<code>const</code>指令聲明的常量也是不提升,同樣存在暫時性死區,隻能在聲明的位置後面使用。
上面代碼在常量<code>max</code>聲明之前就調用,結果報錯。
<code>const</code>聲明的常量,也與<code>let</code>一樣不可重複聲明。
對于複合類型的變量,變量名不指向資料,而是指向資料所在的位址。<code>const</code>指令隻是保證變量名指向的位址不變,并不保證該位址的資料不變,是以将一個對象聲明為常量必須非常小心。
上面代碼中,常量<code>foo</code>儲存的是一個位址,這個位址指向一個對象。不可變的隻是這個位址,即不能把<code>foo</code>指向另一個位址,但對象本身是可變的,是以依然可以為其添加新屬性。
下面是另一個例子。
上面代碼中,常量<code>a</code>是一個數組,這個數組本身是可寫的,但是如果将另一個數組指派給<code>a</code>,就會報錯。
如果真的想将對象當機,應該使用<code>object.freeze</code>方法。
上面代碼中,常量<code>foo</code>指向一個當機的對象,是以添加新屬性不起作用,嚴格模式時還會報錯。
除了将對象本身當機,對象的屬性也應該當機。下面是一個将對象徹底當機的函數。
es5隻有兩種聲明變量的方法:<code>var</code>指令和<code>function</code>指令。es6除了添加<code>let</code>和<code>const</code>指令,後面章節還會提到,另外兩種聲明變量的方法:<code>import</code>指令和<code>class</code>指令。是以,es6一共有6種聲明變量的方法。
全局對象是最頂層的對象,在浏覽器環境指的是<code>window</code>對象,在node.js指的是<code>global</code>對象。es5之中,全局對象的屬性與全局變量是等價的。
上面代碼中,全局對象的屬性指派與全局變量的指派,是同一件事。(對于node來說,這一條隻對repl環境适用,子產品環境之中,全局變量必須顯式聲明成<code>global</code>對象的屬性。)
未聲明的全局變量,自動成為全局對象<code>window</code>的屬性,這被認為是javascript語言最大的設計敗筆之一。這樣的設計帶來了兩個很大的問題,首先是沒法在編譯時就報出變量未聲明的錯誤,隻有運作時才能知道,其次程式員很容易不知不覺地就建立了全局變量(比如打字出錯)。另一方面,從語義上講,語言的頂層對象是一個有實體含義的對象,也是不合适的。
es6為了改變這一點,一方面規定,為了保持相容性,<code>var</code>指令和<code>function</code>指令聲明的全局變量,依舊是全局對象的屬性;另一方面規定,<code>let</code>指令、<code>const</code>指令、<code>class</code>指令聲明的全局變量,不屬于全局對象的屬性。也就是說,從es6開始,全局變量将逐漸與全局對象的屬性脫鈎。
上面代碼中,全局變量<code>a</code>由<code>var</code>指令聲明,是以它是全局對象的屬性;全局變量<code>b</code>由<code>let</code>指令聲明,是以它不是全局對象的屬性,傳回<code>undefined</code>。
感謝阮一峰老師提供的譯文幫助