在學習ES6的塊做用域和 let、const 以前,咱們先來看看ES5之前的 var 關鍵字。chrome
var 關鍵字用於定義一個變量,一般咱們會將其與變量的賦值合併爲一條語句,就像下面這樣(例1):編程
var age = 30;
但實際狀況是有些微妙的。閉包
在JavaScript中,變量的定義與否,雖然不像強類型語言那樣重要,但也仍是有所不一樣的。app
變量未定義,是一種未捕獲類型的錯誤,輸出的結果是變量未定義,同時終止後續腳本的執行,示例以下(例2):編程語言
console.log(age);
輸出:函數
age is not defined.
已經定義但未賦值的變量,輸出的結果是undefined(這個undefined不是未定義,是指變量的值爲undefined,這是JavaSript中的一個特殊的值)。示例以下(例3):學習
var age; console.log(age);
輸出:spa
undefined
var 定義變量語句,在JavaScript中會被編譯器提高到整個做用域內有效,不管它寫在腳本的哪一個位置。示例以下(例4):firefox
console.log(age); var age;
輸出code
undefined
這個待遇只有 var 定義語句都有,賦值語句是不會被提高的。當咱們在代碼中寫下一條定義的同時賦值的 var 語句時,編譯器會將其拆分爲定義語句和賦值語句,並將定義語句提高,而賦值語句仍然只對其後的代碼有效。
請看下面的例子(例5):
console.log(age); var age = 30;
console.log(age);
它與以下例子的輸出是一致的(例6):
var age; console.log(age); age = 30; console.log(age);
輸出結果:
undefined
30
咱們還知道,JavaScript中變量能夠不定義就使用,好比(例7):
age = 30;
console.log(age);
輸出:
30
這種直接賦值的語句,與定義的同時賦值,仍是有所不一樣。參見下面的例子(例8)
console.log(age); age = 30; console.log(age);
輸出結果:
ReferenceError: age is not defined
之因此一例5有這麼大的區別,在於這裏只有變量賦值,沒有 var 定義部分。編譯器不會自動補充一個 var 定義並提高(或者說編譯器補充了一個隱含的 var 定義,可是這個隱含的 var 定義不會被提高),所以第一個console.log語句會直接報出變量未定義的錯誤,以後的變量賦值和輸出語句不被執行。
爲避免誤用未定義的變量,在使用變量前,要養成在使用變量前用 var 進行定義的好習慣:)。除非在函數做用域內要使用父做用域的變量,這也是閉包存在的根源。
若是可能,可使用嚴格模式,即在腳本做用域的第一行,寫下 "use strict"(注意要帶着又引號哦)。這樣作的一個好處是,WebStorm 這樣的 IDE 會在爲未定義的變量賦值時,直接給出警告。
塊做用域是指用一對花括號括住的語句塊使用域。在ES5之前,是沒有塊做用域的,它只有函數做用域,所以 var 在非函數做用域的括號內,與在括號外,沒有區別。爲了保持向下兼容,在ES6中,var 在塊做用域中仍然保持其原有性狀。以下例(例9):
console.log(age); { var age = 30; } console.log(age);
未定義就賦值的變量,也與原來的表現一致(例10)
{ age = 30; } console.log(age);
要使塊做用域別有不同的意義,就須要 let 和 const,這兩個關鍵字了。
let 關鍵字與 var 的做用相似,都是定義變量,也均可以在定義變量的同時,爲變量賦值。不一樣之處有如下兩點:
第一,let 能夠定義(僅在)塊做用域內(有效)的變量。以下例所示(例11):
{ let age = 30; } console.log(age);
輸出結果爲:
ReferenceError: age is not defined
對照例9,能夠看出區別所在。
第二,let 定義變量語句,不會被提高到整個做用域有效。見下例(例12):
console.log(age);
let age = 30;
輸出結果爲:
ReferenceError: age is not defined
對照例4,能夠看出區別所在。
還有一個細節,let 和 var 都有在子做用域內定義與父做用域同名變量時,屏蔽父做用域同名變量的做用。
見以下兩例:
(例13)
var age = 20; (function () { var age = 30; console.log(age); })(); console.log(age);
(例14)
var age = 20; { let age = 30; console.log(age); } console.log(age);
輸出結果均爲:
30 20
區別在於,若是在子做用域中,定義同名變量以前就使用該變量,對於 var 而言,因爲其會被提高,所以獲得的是undefined。對於let,不會被提高,卻也不會使用父做用域的變量,而是報出引用錯誤。示例以下:
(例15)
var age = 20; (function () { console.log(age); var age = 30; })();
輸出:
undefined
(例16)
var age = 20; { console.log(age); let age = 30; }
輸出:
ReferenceError: age is not defined
還要說明一點,在ES6中,全部具備語句塊含義花括號括住的部分都構成塊做用域,包括單純的語句塊、if語句塊、while語句塊、for語句塊、break語句塊和函數語句塊。
在 for 的循環變量定義,會在每次迭代中執行一遍,就至關於每次迭代定義一個變量,迭代結束銷燬,循環多少次,生成多少個不一樣的循環變量。 這個特色的實際意義,能夠用下面的兩個示例說明。
(例17)
var i; var fn = []; for (i = 0; i < 3; i++) { fn.push(function () { console.log(i); }); } fn[0](); fn[1](); fn[2]();
可能會預期其輸出結果爲:
0 1 2
實際輸出結果爲:
3 3 3
(例18)
var fn = []; for (let i = 0; i < 3; i++) { fn.push(function () { console.log(i); }); } fn[0](); fn[1](); fn[2]();
輸出結果爲:
0 1 2
在沒有塊做用域和let關鍵字以前,要實現一樣效果,須要利用閉包(例19)
var i; var fn = []; for (i = 0; i < 3; i++) { fn.push((function (i) { return function () { console.log(i); } })(i)); } fn[0](); fn[1](); fn[2]();
附帶說一句:for 語句塊的循環變量賦值部分的let定義,做用域很特殊,是循環體做用域的父做用域。
例20:
for (let i = 0; i < 3; i++) { let i = "s"; console.log(i); }
輸出結果爲:
s
s
s
輸出三個 s ,說明循環變量 i 保持了從 0 到 3 的自增,而循環體內的 i 屏蔽了循環變量 i ,因此會輸出 s。
因爲 let 相較於 var 優點明顯,又沒有明顯的缺點,在ES6兼容環境下,能夠徹底拋棄 var,而用 let 代替。
《你不知道的JavaScript》中,提倡C語言風格的 let 使用方法,即在做用域的起始處使用 let 定義做用域內的全部變量,這樣能夠避免變量未定義就使用的狀況發生。
我的以爲仍是按現代編程語言就近聲明的慣例更合適,能夠避免無心識的誤用,並且在閱讀時也不須要在代碼間跳躍以尋找變量定義和賦值。不過若是能保持代碼塊的簡短,又能堅持同一變量在同一做用域內不做兩用的原則,其實放在哪兒都可有可無。
對於 const,其實沒有太多須要說明的,常量定義而已。
PS:
還有一個細節:變量在使用時,可能處於如下四種狀態:未聲明、已聲明未初始化、已初始化、已賦值。這裏的初始化,指的是默認賦值undefined。
var 和 let 語句,在聲明變量的同時,若是沒有明確給變量賦值,則被初始化爲undefined,以下兩例能夠證實:
(例21)
var age; console.log(age);
(例22)
let age;
console.log(age)
兩例均輸出:
undefined
前面提到 var 語句會被提高,指的就是初始化這個功能,let 的初始化功能不會被提高。
可是 var 和 let 的變量聲明功能,則是在整個做用域內生效的。即無論 var 和 let 處於做用域內什麼位置,所聲明的變量在整個做用域內都是已聲明狀態。沒有使用 var 和 let 聲明直接賦值的變量,則沒有此待遇,在其賦值前,變量未聲明。
下面舉例說明:
未聲明(例23)
console.log(age); age = 30;
console.log(age);
輸出結果爲:
ReferenceError: age is not defined
已聲明未初始化(例24):
console.log(age); let age = 30; console.log(age);
firefox 輸出:
ReferenceError: can't access lexical declaration `a' before initialization
chrome 的輸出比較含混:
ReferenceError: age is not defined
已初始化(例25)
console.log(age); var age = 30; console.log(age);
輸出:
undefined
30
對於已聲明未初始化和未聲明,能夠用 typeof 運算符來作區分,未聲明的變量 typeof 的結果爲 undefined,而對於已聲明未初始化的變量,typeof 則會拋出 ReferenceError。
未聲明(例26)
console.log(typeof age); age = 30; console.log(typeof age);
輸出結果爲:
undefined
number
已聲明未初始化(例27)
console.log(typeof age); let age = 30; console.log(typeof age);
chrome 輸出:
ReferenceError: age is not defined
firefox 輸出:
ReferenceError: can't access lexical declaration `a' before initialization
對於未聲明和已聲明且已初始化爲 undefined 的變量,typeof 就力不從心了,由於 typeof undefined 的結果也是 undefined,和未聲明變量的 typeof 結果相同。見下例(例28)
console.log(typeof age); age = 30; console.log(typeof name); var name;
輸出:
undefined
undefined
還想區別,就用以下方式(例29):
console.log(typeof age && undefined === age);
對於已聲明且已初始化爲 undefined 的變量,輸出結果爲 true。
不然,chrome 拋出 ReferenceError: age is not undefined,firefox 拋出