塊作用域
是一個(gè)用來(lái)對(duì)之前的最小授權(quán)原則進(jìn)行擴(kuò)展的工具蹂随,將代碼從在函數(shù)中隱藏信息擴(kuò)展為在塊中隱藏信息咱台。
【之前的代碼】:
for(var loop = 0; loop < 10; loop++) {
console.log(loop);
}
【說(shuō)明】:我們?cè)?for 循環(huán)的頭部直接定義了變量 loop图焰,通常是因?yàn)橹幌朐?for 循環(huán)內(nèi)部的上下文中使用 loop盐碱,而忽略了 loop 實(shí)際上會(huì)被綁定在所處的作用域中榛了。如果能夠?qū)?loop 聲明在 for 循環(huán)內(nèi)部會(huì)是一個(gè)很有意義的事情,而現(xiàn)在只能依靠自覺(jué)性來(lái)防止自己沒(méi)在作用域其他地方意外地使用 loop 變量抢肛。也正是基于這個(gè)原因狼钮,ES6 推出了塊作用域的概念。
【塊作用域的用處】:變量的聲明應(yīng)該距離使用的地方越近越好捡絮,并最大限度地本地化熬芜。
with
with 可以創(chuàng)建一個(gè)詞法作用域,將對(duì)象的屬性作為聲明在當(dāng)前作用域的變量(函數(shù))锦援。換言之猛蔽,with 從對(duì)象中創(chuàng)建的作用域僅在 with 聲明中而非外部作用域中有效剥悟。不可不推薦使用 with 來(lái)實(shí)現(xiàn)塊作用域灵寺,在這里提出只是為了說(shuō)明 with 有這個(gè)功能。
try/catch
JavaScript 的 ES3 規(guī)范中規(guī)定 try/catch 的 catch 分句會(huì)創(chuàng)建一個(gè)塊作用域区岗,其中聲明的變量?jī)H在 catch 內(nèi)部有效略板。
【注意】:其中聲明的變量是指 catch(variable),括號(hào)內(nèi)的變量慈缔,而不是在 {} 中聲明的變量叮称。
【示例】:
try {
undefined();
} catch(err) {
var name = "spirit";
console.log(err);
}
console.log(name); // spirit
console.log(err);
let
let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { ... } 內(nèi)部)。換句話說(shuō)藐鹤,let 為其聲明的變量隱式地劫持了所在的塊作用域瓤檐。
【示例】:
var foo = true;
if(foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar); // ReferenceError
【注意】:用 let 將變量附加在一個(gè)已經(jīng)存在額塊作用域上的行為是隱式的。也就是說(shuō)娱节,在開(kāi)發(fā)和修改代碼的過(guò)程中挠蛉,如果沒(méi)有密切關(guān)注哪些塊作用域中有綁定的變量,并且習(xí)慣性地移動(dòng)這些塊或者將其包含在其他的塊中肄满,將會(huì)導(dǎo)致代碼變得混亂谴古。
【解決方案】:為塊作用域顯式地創(chuàng)建塊可以部分解決這個(gè)問(wèn)題,使變量的附屬關(guān)系變得更加清晰稠歉。
var foo = true;
if(foo) {
{ // 顯示的塊
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar); // ReferenceError
【解釋】:只要聲明是有效的掰担,在聲明中的任意位置都可以使用 {} 括號(hào)來(lái)為 let 創(chuàng)建一個(gè)用于綁定的塊。
【注意】:使用 let 進(jìn)行的聲明不會(huì)在塊作用域中進(jìn)行提升怒炸。聲明的代碼被執(zhí)行之前带饱,聲明并不存在。
{
console.log(bar); // ReferenceError!
let bar = 2;
}
1. 垃圾收集
另一個(gè)塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)阅羹。
【示例】:
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
var someReallyBigData = {
// ...
};
process(someReallyBigData);
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, false);
【問(wèn)題】:click 函數(shù)的點(diǎn)擊回調(diào)并不需要 someReallyBigData 變量纠炮。理論上這意味著當(dāng) process() 執(zhí)行后月趟,在內(nèi)存中占用大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了。但是恢口,由于 click 函數(shù)形成了一個(gè)覆蓋整個(gè)作用域的閉包孝宗,JavaScript 引擎極有可能依然保存著這個(gè)結(jié)構(gòu)(取決于具體實(shí)現(xiàn))。
【示例】:
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個(gè)塊中定義的內(nèi)容完事可以銷毀耕肩!
{
let someReallyBigData = {
// ...
};
process(someReallyBigData);
}
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
}, false);
【解釋】:使用塊作用域可以讓引擎清楚地知道沒(méi)有必要繼續(xù)保存 someReallyBigData 因妇。
【建議】:為變量顯式聲明塊作用域,并對(duì)變量進(jìn)行本地化綁定是非常有用的工具猿诸,可以把它添加到你的代碼工具箱中了婚被。
2. let 循環(huán)
【示例】:
for(let loop = 0; loop < 10; loop++) {
console.log(loop);
}
console.log(loop); // ReferenceError
// 等價(jià)于
{
let j;
for(j = 0; j < 10; j++) {
let i = j; // 每個(gè)迭代重新綁定!
console.log(i);
}
}
【解釋】:for 循環(huán)頭部的 let 不僅將 loop 綁定到了 for 循環(huán)的塊中梳虽,事實(shí)上它將其重新綁定到了循環(huán)的每一個(gè)迭代中址芯,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。
【注意】:由于 let 聲明附屬于一個(gè)新的作用域而不是當(dāng)前的函數(shù)作用域(也不屬于全局作用域)窜觉,當(dāng)代碼中存在對(duì)于函數(shù)作用域中 var 聲明的隱式依賴時(shí)谷炸,就會(huì)有很多隱藏的陷阱,如果用 let 來(lái)替代 var 則需要在代碼重構(gòu)的過(guò)程中付出額外的精力禀挫。
const
除了 let 以外旬陡,ES6 還引入了 const,同樣可以用來(lái)創(chuàng)建塊作用域變量语婴,但其值是固定的(常量)描孟。之后任何視圖修改值的操作都會(huì)引起錯(cuò)誤。
小結(jié)
- 塊作用域指的是變量和函數(shù)不僅可以屬于所處的作用域砰左,也可以屬于某個(gè)代碼塊(通常指 {} 內(nèi)部)匿醒。
- 從 ES3 開(kāi)始,try/catch 結(jié)構(gòu)在 catch 分句中具有塊作用域缠导。
- 在 ES6 中引入了 let 關(guān)鍵字廉羔,用來(lái)在任意代碼塊中聲明變量。
- 有些人認(rèn)為塊作用域不應(yīng)該完全作為函數(shù)作用域的替代方案酬核。兩種功能應(yīng)該同時(shí)存在蜜另,開(kāi)發(fā)者可以并且也應(yīng)該根據(jù)需要選擇使用何種作用域,創(chuàng)造可讀嫡意、可維護(hù)的優(yōu)良代碼举瑰。
附錄:塊作用域的替代方案
從 ES3 發(fā)布以來(lái),JavaScript 中就有了塊作用域蔬螟,而 with 和 catch 分句就是塊作用域的兩個(gè)小例子此迅。
【示例】:
{
let a = 2;
console.log(a); // 2
}
console.log(a); // ReferenceError
【問(wèn)】:如何在 ES6 之前也可以實(shí)現(xiàn)上述示例的效果。
【答】:使用 catch。
try { throw 2; } catch(a) {
console.log(a); // 2
}
console.log(a); // ReferenceError
【解釋】:利用 catch 分句具有塊作用域來(lái)實(shí)現(xiàn)耸序,除了丑陋且奇怪了一些忍些,還是挺好用的。
性能
- 首先坎怪,try/catch 的性能的確很糟糕罢坝,但技術(shù)層面上沒(méi)有合理的理由來(lái)說(shuō)明 try/catch 必須這么慢,或者會(huì)一直慢下去搅窿。自從 TC39 支持在 ES6 的轉(zhuǎn)換器中使用 try/catch 后嘁酿,Traceur 團(tuán)隊(duì)已經(jīng)要求 Chrome 對(duì) try/catch 的性能進(jìn)行改進(jìn),他們顯然有很充分的動(dòng)機(jī)來(lái)做這件事情男应。
- 其次闹司,IIFE 和 try/catch 并不是完全等價(jià)的,因?yàn)槿绻麑⒁欢未a中的任意一部分拿出來(lái)用函數(shù)進(jìn)行包裹沐飘,會(huì)改變這段代碼的含義游桩,其中的 this、return耐朴、break 和 contine 都會(huì)發(fā)生變化借卧。IIFE 并不是一個(gè)普適的解決方案,它只適合在某些情況下進(jìn)行手動(dòng)操作隔箍。
- 最后問(wèn)題就變成了:你是否想要塊作用域谓娃?如果你想要脚乡,這些工具就可以幫助你蜒滩。如果不想要,繼續(xù)使用 var 來(lái)寫代碼就好了奶稠。