前言:塊綁定
在傳統(tǒng)意義上,變量聲明工作的方式在Js一直是棘手的編程部分,在大多數(shù)基于C語言的編程語言中,變量(或綁定)被創(chuàng)造在聲明出現(xiàn)的地方,然而在Js中情況并不是這樣的。在Js中,你的變量實際上被創(chuàng)造依賴于你怎樣聲明它們,并且在ECMAScript6中提供了更容易控制作用域的選擇。這一章闡述了為什么經(jīng)典的var聲明是易混淆的,介紹了在ECMAScript6中的塊級綁定,并且提供了一些特別好的使用它們的實例。
var 聲明和提升
用var聲明的變量無論真實的聲明出現(xiàn)在函數(shù)的哪個地方都被處理好像它們處于函數(shù)的頂部(或者在全局作用域,如果它是定義在一個函數(shù)的外面),這被稱為提升,對于提升的作用的實例,思考下面這個函數(shù)定義:
function getValue(condition){ if(condition){ var value = "blue"; //other code return value; } else{ //value exsits here with a value of undefined return null; } //value exsits here with a value of undefined }
如果你不熟悉Js,你可能會期望變量 value僅僅在condition這個條件為真的情況下被創(chuàng)建。而事實上,變量value是無論如何都會被創(chuàng)建。在這個函數(shù)情形下,Javascript引擎改變了getValue函數(shù),像下面這樣:
function getValue(condition) { var value; if(condition){ value = "blue"; // other code return code; } else{ return null; } }
value的聲明被提升到頂部,然而初始化仍然在相同的地方。這意味著變量value實際上在else子句中仍然是可訪問的。如果從else子句訪問,由于未初始化,變量將僅僅有一個undefined的值。
這常常花費新的Js開發(fā)者一些時間去適應(yīng)聲明提升,并且常常誤解獨特的行為會最終導(dǎo)致bugs.為此,ECMAScript6提出了塊級作用域選項去更好地控制 一個變量的生命周期。
塊級聲明
塊級聲明是指在函數(shù)中聲明的變量在給定塊級范圍外是不可訪問的。塊級作用域也稱為詞作用域,被這樣創(chuàng)建:
1.在一個函數(shù)的內(nèi)部
2.在一個塊的內(nèi)部(被字符 { 和 } 標識)
塊級作用域是許多基于C語言的編程語言的工作方式,并且在ECMAScript6中塊級聲明的提出旨在為Javascript提出相同的靈活性(和一致性)。
Let 聲明
let 的聲明語法和 var 的語法相同。你基本上能用 let 替換 var 去聲明一個變量, 但是限制變量的作用域僅僅在當(dāng)前代碼塊(有一些其他微妙的差異也會在之后討論)。由于 let 聲明不被提升在封閉塊的頂部,你可能總是想放 let 聲明在封閉塊的最開始位置,以便使它們是可用的在整個塊內(nèi)是可訪問的。下面是個例子:
function getValue(condition) { if (condition) { let value = "blue"; // other code return value; } else { //value doesn't exist return null; } // value does't exist }
這個版本的 getValue 函數(shù)的運作更接近于你所期望的它在基于C的編程語言中的實現(xiàn)。因為變量 value 被聲明用 let 而不是 var,這個聲明將不會提升到函數(shù)定義的頂部,并且一旦函數(shù)執(zhí)行到 if 塊的外面,變量 value 將不再是可訪問的, 如果 condition 條件為 false,那么 value 將從不會聲明和初始化。
無重復(fù)聲明
如果一個標識符在一個作用域中早已被定義,然后在這個作用域中用 let 聲明這個標識符會造成一個錯誤拋出。舉例:
var count = 30; // Syntax error let count = 40;
在這個例子中,count 被聲明兩次:一次用 var ,一次用 let。因為 let 將不會重新定義一個已經(jīng)在相同作用域內(nèi)存在的標識符,所以let 聲明將拋出一個錯誤。另一方面,如果 一個 let 聲明在變量的包含作用域內(nèi)以同樣的名字創(chuàng)建一個新的變量將沒有錯誤拋出,如下面代碼所述:
var count = 30; //Does not throw an error if (condition) { let count = 40; //more code }
這個 let 聲明沒有拋出錯誤是因為它是在 if 語句范圍中創(chuàng)建了一個名為 count 的新的變量,而不是在外圍代碼塊中創(chuàng)建的。在 if 代碼塊內(nèi)部,這個新的變量覆蓋了全局變量 count ,阻止訪問這個全局變量直到執(zhí)行流離開這個 if 語句這個代碼塊 。
常量聲明
在ECMAScript6中你也能用 const 聲明語法定義一個變量。用 const 聲明的變量被當(dāng)做一個常量,這意味著它們的值一旦被設(shè)定將不能改變。為此,每一個 const 變量必須在聲明的時候進行初始化,如下所示:
//Valid constant const maxItems = 30; //Syntax error: missing initialization const name;
因為maxItem變量被初始化了,所以它的 const 聲明應(yīng)該沒有問題地工作。然而如果你嘗試去運行包含這個代碼的程序, name 變量將造成一個語法錯誤,這是因為 name 變量沒有被初始化。
常量聲明 vs Let聲明
常量聲明,像 let 聲明,是塊級聲明。這意味著一旦執(zhí)行流運行到常量被聲明的代碼塊的外面時,常量將不再是可訪問的,并且聲明不被提升,如下所示:
if (condition) { const maxItem = 5; // more code } // maxItem isn't accessible here
在這段代碼中,常量 maxItem 在 if語句中被聲明。一旦這個語句結(jié)束執(zhí)行,在代碼塊的外面 maxItem 將不再是可訪問的。
另一個和 let 相似的地方是:當(dāng) const 聲明一個在相同的作用域內(nèi)早已被定義的變量會拋出一個錯誤。如果這個變量用 var 聲明(對于全局作用域或函數(shù)作用域)或者用 let 聲明(對于塊作用域內(nèi)部),則它是無關(guān)緊要的。舉例,思考下面代碼:
var message = "hello!" let age = 25; // Each of these would throw an error. const message = ''Goodbye!'; const age = 30;
這兩個 const 聲明單獨來說是有效的,但是鑒于在這個事件中前面的 var 和 let 聲明,這兩個聲明都將如預(yù)期所示不工作。
盡管存在這些相同點,但是 let 和 const 之間有一個很大的不同之處需要牢記。在所有嚴格和非嚴格模式下企圖給一個先前定義過的 const 常量賦值將拋出一個錯誤,:
const maxItem = 5; maxItem = 6; //throw error
在一個方面很像在其它語言中的常量,maxItem 變量之后不會被賦新值。然而,在另一方面不像在其他語言中的常量,如果一個常量的值是一個對象可能會被修改。
用 const 聲明對象
一個 const 聲明防止綁定的修改和不是它本來的的值。那意味著 const 對于對象的聲明不會阻止那些對象的修改。舉例:
const person = { name: "Nicholas" }; // works person.name = "Greg"; //throws an error person = { name: "Greg" }
在被綁定的 person 被創(chuàng)建用一個對象屬性的初始化值。它是可能的去改變 person.name 沒有導(dǎo)致錯誤,這是因為它只是去改變 person 所包含的屬性并且并沒有改變被綁定的 person 。當(dāng)這段代碼試圖賦予一個值給 person(因此嘗試去改變這個綁定),一個錯誤將被拋出。這個 const 如何工作的微妙處是容易被誤解的。只需要記住: const 防止綁定的修改,不是防止綁定的對象的屬性值的修改。
時域死區(qū)
一個用 let 或 const 聲明的變量不能被訪問直到這個變量被聲明后。嘗試這樣做將會造成引用錯誤,甚至當(dāng)正常地使用安全操作比如在這個例子中使用 typeof 操作符也會造成錯誤:
if (condition) { console.log(typeof value); //ReferenceError let value = "blue"; }
這里,變量 value 用 let聲明和初始化,但是這個語句從不執(zhí)行因為前一行拋出了錯誤。這個問題在Javascript社區(qū)被稱為時間死區(qū)。時間死區(qū)在ECMAScript規(guī)范中未被明確地命名,但是這個術(shù)語被用來描述為什么 let變量 和 const 變量不是可訪問在它們未被聲明前。這部分涉及了一些時間死區(qū)造成的聲明位置的微妙處,并且盡管例子展示全部使用了 let 聲明,注意同樣的信息適用于 const。
當(dāng)Javascript引擎瀏覽一個即將執(zhí)行的代碼塊并且發(fā)現(xiàn)一個變量聲明,它要么提升這個聲明到函數(shù)或全局作用域的頂部(對于 var),要么放這個聲明到時間死區(qū)(對于 let 和 const).任何嘗試在時間死區(qū)訪問一個變量都會造成運行時錯誤。一旦執(zhí)行流進行到變量聲明的位置,那個變量才被移除時間死區(qū)因而可以安全使用。
這是正確的做法當(dāng)你嘗試去使用用 let 或 const 聲明的變量在它并未被定義前。像之前的例子所闡述的,這個甚至正常地應(yīng)用在安全操作符 typeof .然而你能用 typeof 在一個變量被聲明的封閉塊的外面去檢測這個變量的類型,盡管它可能不會給你之后聲明的這個變量的結(jié)構(gòu)。思考這個代碼:
console.log(typeof value); // "undefined" if (condition) { let value = 'blue"; }
當(dāng) typeof 操作符執(zhí)行時這個變量 value 不是在時間死區(qū),因為它出現(xiàn)在變量 value 被聲明的封閉塊的外面。這意味著沒有變量綁定,并且 typeof 操作符簡單地返回 undefined。
時間死區(qū)僅僅是塊綁定的一個獨特的地方。另一個不得不說的獨特的地方在它們在循環(huán)中的用法。
在循環(huán)中的塊綁定
也許開發(fā)者最想讓變量的塊級作用域存在的地方是在 for 循環(huán)中,在這種情形下,計數(shù)器變量意味著只被在循環(huán)中使用。舉例來說,它是非常普遍的在JavaScript中這樣的代碼:
for (var i = 0; i<10; i++){ process(item[i]); } // i is still accessible here console.log(i); //10
在其他語言中,默認是塊級作用域,這個例子按預(yù)期的工作,并且僅僅對于for 循環(huán)可以訪問到變量 i 。然而在JavaScript中,變量 i在循環(huán)被完成之后仍然是可訪問的,因為 var 變量獲得提升。相反用 let,在下面的代碼中應(yīng)該得到這樣的結(jié)果:
for (let i = 0; i < 10; i++ ) { process(items[i]); } // i is not accessible here --throw an error console.log(i);
在這個例子中,變量 i 僅僅在 for 循環(huán)中存在,一旦循環(huán)完成,這個變量任何其他地方不再是可訪問的。
在循環(huán)中的函數(shù)
var 的特點長期以來造成了在循環(huán)內(nèi)創(chuàng)建函數(shù)的問題,因為循環(huán)變量在循環(huán)體作用域的外面是可訪問的思考下面這個例子:
var funcs = []; for (var i = 1; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.fotEach(function(func) { func(); // output the number "10" ten times });
你可能通常希望這段代碼輸出數(shù)字0-9,但是它輸出的是數(shù)字10十次在一行。那是因為 i 在每一次的循環(huán)迭代中是共享的,意味著在循環(huán)內(nèi)創(chuàng)建的方法總是持有對同一變量的引用。這個變量 i 一旦循環(huán)結(jié)束則擁有值10,因而當(dāng)console.log被執(zhí)行時,在循環(huán)中每次值10被打印。
為了去修復(fù)這個問題,開發(fā)者在循環(huán)中用立即調(diào)用函數(shù)表達式(IIFE)去強制創(chuàng)建一個他們想要循環(huán)訪問的新的變量副本 ,如下所示:
var funcs = []; for (var i = 0; i < 10; i++) { funcs.push(function(value) { return function() {console.log(value);} }(i)); } funcs.forEach(function(func) { func(); });
這個版本在循環(huán)的內(nèi)部用一個IIFE,i 變量被傳遞給立即執(zhí)行函數(shù),在立即執(zhí)行函數(shù)中創(chuàng)建它的副本并且以value 變量存儲它。這是使用那個迭代函數(shù)的意義,因此每次調(diào)用函數(shù)返回了預(yù)期的循環(huán)計數(shù)從0到9的值。幸運的是,用ECMAScript6中的 let 和 const 的塊級綁定對于你來說可以簡化這個循環(huán)。
在循環(huán)中的Let 聲明
一個 let 聲明通過有效地模仿IIFE在上一個例子中所做的簡化了循環(huán)。在每次迭代中,這個循環(huán)創(chuàng)建了一個新的變量并且初始化變量的值用上一次迭代所使用的的相同名字。那意味著你能完全省略IIFE并且獲得你所期望的結(jié)果,像這樣:
var funcs = []; for (let i = 0; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.forEach(function(func) { func(); // outputs 0, then 1, then 2, up to 9 })
這個循環(huán)的實現(xiàn)的確像使用 var 和IIFE的循環(huán),但是可以說更簡潔。let 聲明通過循環(huán)每次創(chuàng)建了一個新的變量 i,因此在循環(huán)體中創(chuàng)建的每個方法 獲得了它自己i 的副本。每個 i 的副本有它在循環(huán)的迭代開始(即它被創(chuàng)建的那個迭代)被分配的值。對于for-in 和 for-of 是同樣的道理,如這兒所示:
var funcs = [], object = { a: true, b: true, c: true }; for (let key in object) { funcs.push(function() { console.log(key); }); } funcs.forEach(function(func) { func(); // outputs "a", then "b", then "c" });