在寫js代碼時,不少人會使用var關鍵字來聲明變量。var關鍵字聲明的變量使得咱們不管在函數做用域仍是全局做用域中任意地方聲明的一個變量都會被當成在當前做用域的頂部聲明的,這就是咱們常說的變量提高(Hoisting)機制。用一個函數舉例子:es6
function example(flag) {
console.log(value) //此處可訪問變量value,值爲undefined
if (flag) {
var value = 'hello'
console.log(value) //此處可訪問變量value,值爲hello
} else {
console.log(value) //此處可訪問變量value,值爲undefined
}
}
複製代碼
不瞭解js的人可能認爲只有當flag爲true的時候才能訪問到value變量,但其實var聲明的value變量已經被提高到了example函數做用域的頂部了,這樣在此函數的任意位置都是能夠訪問到value變量的,只不過只有當flag爲true是value纔會被初始化纔有值。在預編譯階段,js引擎會將上面的example 函數解析成下面這樣:瀏覽器
function example(flag) {
var value
console.log(value) //此處可訪問變量value,值爲undefined
if (flag) {
var value = 'hello'
console.log(value) //此處可訪問變量value,值爲hello
} else {
console.log(value) //此處可訪問變量value,值爲undefined
}
}
複製代碼
剛接觸js的開發者一般會花一下時間來習慣變量提高,有時還會由於誤解致使程序中出現bug。爲此es6中引入塊級做用域來強化對變量生命週期的控制。安全
一般若咱們須要一個變量只能在指定塊中訪問時會用到塊級聲明。塊級做用域(也稱詞法做用域)存在於:bash
let的聲明的var的聲明語法相同。經過let聲明的變量只能在當前做用域只訪問到,而且let聲明的變量不會被提高,所以開發者一般會把let聲明的變量放在做用域頂部以便整個做用域能夠訪問。如下是let聲明的示例:函數
function example(flag) {
console.log(value) //此處不存在變量value
if (flag) {
console.log(value) //此處不存在變量value
let value = 'hello'
console.log(value) //此處可訪問變量value,值爲hello
} else {
console.log(value) //此處不存在變量value
}
}
複製代碼
示例中let聲明的變量value不會被提高到頂部,執行流離開if塊,value就會馬上被銷燬,若是flag爲false那麼value變量將永遠不會被聲明和初始化。ui
若是let關鍵字聲明瞭重複的某個標識符,那麼就會拋出報錯,舉個例子:spa
var count = 30
//拋出語法錯誤
let count = 40
複製代碼
在這個示例中,如上所說let不能重複聲明已經存在的標識符,因此此處let聲明會報錯。可是若是let 聲明的count變量的當前做用域被內嵌在另外一個做用域就能夠被正常訪問,好比:code
var count = 30
if (flag) {
//不會拋出語法錯誤
let count = 40
}
複製代碼
此處的let 聲明的count只在做用域if塊中能被訪問,而且在if塊中let聲明的count 會覆蓋外面var聲明的count變量對象
const是用來聲明常量的關鍵字,其值一旦被肯定就不能被更改,因此每一個經過const聲明的常量必須進行初始化。生命週期
//有效常量
const maxItems = 30
//語法錯誤: 常量未初始化
const name;
複製代碼
const與let聲明的都是塊級標識符,因此const聲明的常量只能在當前做用域訪問到也不會被提高到頂部。一樣,const也不能聲明重複的標識符,不管該標識符時使用var(在全局或函數做用域中),仍是let(在塊級做用域中)聲明的。舉例來講:
var maxItems = 30
let name = 'coco'
//這兩條語句都會拋出錯誤
const maxItems = 60
const name = 'koko'
複製代碼
儘管const和let類似之處不少,可是他們仍是有一個最大的區別,就是const聲明的常量不能夠再次被賦值。es6中的常量這一點和其餘語言很像,然而,與其餘語言不一樣的是若是js中常量是對象,則對象中的值能夠修改。
const聲明不容許修改綁定,但容許修改值,這就意味着用const生命的對象後,能夠修改對象的屬性值。舉個例子:
const dog = {
name: 'koko'
}
//能夠修改對象屬性的值
dog.name = 'coco'
//拋出語法錯誤
dog = {
name: 'coco'
}
複製代碼
在這段代碼中修改dog的屬性不會報錯,由於修改的是dog包含的值,dog的綁定並無變。但若是改變的dog的綁定就會報錯
let和const與var不一樣,若是要在let或const聲明變量以前訪問這些變量,即便是相對安全的typeof操做也會觸發引用錯誤,好比下面這段代碼:
if (flag) {
console.log(typeof (value)); //引用錯誤
let value = 'b'
}
複製代碼
由於console.log(typeof (value))拋出錯誤,因此value的聲明和初始化不會被執行,此時的value還位於js社區所謂的「臨時死區」(TDZ)中,人們經常使用他來描述let和const的不提高效果。(這裏以let爲例,可是const也是如此)。js引擎在掃描代碼發現變量聲明時,要麼將他們提高到做用域頂部(遇到var聲明),要麼將聲明放入TDZ中(遇到let和const聲明)。訪問TDZ中的變量會觸發運行時錯誤。只有執行過變量聲明語句後,變量纔會從TDZ中移出,而後方可正常訪問。
從上例可看出,即使是不容易出錯的typeof也沒法阻止引擎拋出錯誤,可是在let聲明的做用域外對該變量使用typeof則不會報錯,好比:
console.log(typeof (value)); //"undefined"
if (flag) {
let value = 'b'
}
複製代碼
typeof是在聲明變量value代碼塊外執行的,此時value並不在TDZ中。這也就意味着不存在value這個綁定,typeof操做最終返回‘undefined’。TDZ只是塊級綁定的特點之一,而在循環中使用塊級綁定也是一個特點。
開發者應該最但願實現for循環中的塊級做用域了,由於這樣他們能夠將隨意聲明的計數器變量限制在for循環中使用。像下面這種代碼在js中是很常見的:
for (var i = 0; i < 10; i++) {
process(items[i]);
}
console.log(i) //在這裏任然能夠訪問變量i
複製代碼
示例代碼中由於計數器變量i是由var聲明的因此變量i會被提高至for循環外的做用域頂部,因此即使是在循環以外也能夠訪問變量i,但若是換用let聲明就能夠達到想要的效果:
for (let i = 0; i < 10; i++) {
process(items[i]);
}
console.log(i) //在這裏不能夠訪問變量i,拋出錯誤
複製代碼
在上例中,let聲明的變量i只在for循環中可訪問。在其它語言中(默認擁有塊級做用域的)上面兩個示例也能夠正常運行,可是變量i始終只在for循環中可訪問。
長久以來,使用var聲明會讓開發者在循環中建立函數變得很困難,由於每一次的循環的變量到了下一次循環任然可訪問。好比:
var funArr = []
for (var i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
funArr.forEach(function (func) {
func() //輸出10次數字10
})
複製代碼
上例中可能你想要的是輸出0-9,但是他卻一連串輸出了10次10。這是由於循環裏的每次迭代同時共享着變量i,循環內部建立的函數全都保留了對相同變量的引用(細細品味)。循環結束的時候i最後被賦予的值是10,因此以前循環中內部建立的函數裏引用的變量i的值都是10,每次調用console.log(i)時就會輸出數字10。
爲此,開發者們再循環中使用當即調用函數表達式(IIFE),以強制生成計數器變量的副本,就像這樣:
var funArr = []
for (var i = 0; i < 10; i++) {
funArr.push((function (value) {
return function () {
console.log(value)
}
}(i)))
}
funArr.forEach(function (func) {
func() //輸出0-9
})
複製代碼
上例中,IIFE表達式會爲每次循環的變量i生成一個副本(參數value變量),變量value替代了每次循環中建立的函數中引用的變量i,且value的值爲本次循環中i的值,每次循環生成的value都只在本次循環中可訪問。所以調用函數就會生成咱們所指望的0-9。es6中let和const提供的塊級綁定讓咱們無需再這麼折騰。
let聲明和上例IIFE所作的事相似,也是每次迭代循環都會建立一個新變量,並以以前迭代中同名變量的值將其初始化。可是代碼相對簡潔。代碼示例:
var funArr = []
for (let i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
funArr.forEach(function (func) {
func() //輸出0-9
})
複製代碼
上例代碼中每次循環let聲明都會建立一個新變量i並初始化爲i的當前值,因此每次循環建立的函數都能獲得屬於他們本身的i的副本。對於for-in循環和for-of循環來講也是同樣的,示例以下:
var funArr = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funArr.push(function () {
console.log(key)
})
}
funArr.forEach(function (func) {
func() //輸出a,b,c
})
複製代碼
上例中for-in循環與上上例中for循環表現得行爲一致,也是爲每次循環中建立的函數賦予一個新變量(上例中爲變量key)。但若是使用var聲明則這些函數都會輸出‘c’。
let聲明在循環內部的行爲是標準中專門定義的,他不必定與let的不提高特性相關,理解這一點相當重要。事實上,早期的let 實現不包含這一行爲,他是後來加入的。
es6中沒有明確指明循環中不容許使用const聲明,在不一樣類型的循環中使用const聲明它會表現出不一樣的行爲。普通的for循環能夠在初始化變量時使用const,可是一旦這個變量的值發生改變就會報錯,就像這樣:
var funArr = []
//完成一次迭代後拋出錯誤
for (const i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
複製代碼
在上例中,由const聲明的常量i,在循環第一次迭代中,i=0迭代成功,而後執行i++試圖改變常量i的值,因此會拋出錯誤。所以,const聲明適用於後續循環不會修改該變量的循環中。
在for-in和for-of循環中使用const時的行爲與使用let一致,下面這段代碼應該不會產生錯誤:
var funArr = [],
object = {
a: true,
b: true,
c: true
};
//不會產生錯誤
for (const key in object) {
funArr.push(function () {
console.log(key)
})
}
funArr.forEach(function (func) {
func() //輸出a, b, c
})
複製代碼
這段代碼與上上段代碼幾乎一致,惟一的區別是,循環內不能改變key的值。以前提到過,const聲明的常量不能夠修改他的綁定,可是能夠修改常量的綁定的值(const聲明的對象的屬性)。之因此能夠運用在for-in和for-of循環中,是由於每次迭代不會(像前面for循環的例子同樣)修改已有綁定,而是會建立一個新的綁定。
let和const與var的另外一個區別就是他們在全局做用域中的行爲。當var被用於全局做用域時,它會建立一個新的變量做爲全局對象的屬性(全局對象一般是瀏覽器環境中的window對象)。這意味着var極可能會無心中覆蓋一個已經存在的全局屬性,就像這樣:
//在瀏覽器中爲全局對象window建立全局屬性RegExp
var RegExp = "hello";
console.log(window.RegExp); //「hello」
//全局屬性RegExp被覆蓋
var RegExp = "hi"
console.log(window.RegExp); //「hi」
複製代碼
即使全局變量RegExp是定義在全局對象window上,也不能倖免於被var聲明覆蓋。示例中聲明的全局變量RegExp會覆蓋一個已經存在的全局屬性。js過去一直都是這樣。
若是在全局做用域中使用let或const聲明,他們會在全局做用域下建立一個新的綁定,但該綁定不會添加爲全局對象的屬性。換句話說,let和const不會覆蓋全局變量,只會遮蓋他。示例以下:
let RegExp = "hello";
console.log(RegExp); //「hello」
console.log(window.RegExp === RegExp) //false
const ncz = "hi";
console.log(ncz); //「hi」
console.log("ncz" in window) //false
複製代碼
這裏let聲明的RegExp建立了一個綁定並遮蔽了全局RegExp的變量。結果是window.RegExp和RegExp不相同,但不會破壞全局做用域。const也是同樣。若是不想爲全局對象建立屬性使用let和const聲明要安全不少。
若是但願在全局對象下定義變量,任然可使用var。這種狀況常見於跨frame或跨window訪問代碼。
默認使用const,當須要改變變量的值時使用let。由於大部分變量初始化後值不該再改變,而預料外的變量值的改變是不少bug的源頭。