閉包 詞法作用域 變量提升
變量提升
什么是變量提升
顧名思義,變量提升指的是,在聲明變量的時(shí)候,變量的聲明位置會(huì)被提升至當(dāng)前作用域最前面。
看這個(gè)例子
var foo = "before";
function bar() {
if (!foo) {
var foo = "after";
}
console.log(foo);
}
bar();
由于變量提升,這里輸出的值為 "after"。
也就是說(shuō),上面的代碼其實(shí)等價(jià)于下面的代碼
var foo = "before";
function bar() {
var foo; // foo == undefined
if (!foo) { // 判斷成立
foo = "after";
}
console.log(foo);
}
bar();
變量提升會(huì)提到哪里呢?在 js 里,只有函數(shù)級(jí)(function level)的作用域,所以上面代碼中的 foo 被提到了函數(shù)的頂部。
如果上面沒(méi)有函數(shù),則作為全局變量。
為什么有變量提升
這是一個(gè)歷史問(wèn)題,Javascript 語(yǔ)言設(shè)計(jì)者為了實(shí)現(xiàn)相互遞歸定義,參考了《SICP》4.1.6 節(jié)中給出的解決方式,也就是變量提升。
Brendan Eich 的原話(huà)為
Function declaration hoisting is for mutual recursion & generally to avoid painful bottom-up ML-like order
那什么是相互遞歸定義呢?看下面這部分代碼。
function is_even(n) {
if (n == 0) {
return true;
} else {
return is_odd(n - 1);
}
}
is_even(2); // true
function is_odd(n) {
if (n == 0) {
return false;
} else {
return is_even(n - 1);
}
}
is_odd 和 is_even 函數(shù)互相調(diào)用對(duì)方,按理說(shuō) is_even 定義的時(shí)候 is_odd 還沒(méi)有定義,應(yīng)該會(huì)報(bào)錯(cuò),但由于有變量提升,所以就可以正常執(zhí)行了。
詞法作用域和閉包
先來(lái)看這樣一段代碼
var a = "before";
function foo(){
console.log(a);
}
function bar(fun){
var a = "after";
fun();
}
bar(foo);
輸出結(jié)果為 "before"。
為什么不是根據(jù)“就近原則”選擇變量 a 呢?
這是由于 JS 采用的是詞法作用域 (lexical scoping),又叫靜態(tài)作用域 (static scoping)。
也就是說(shuō),變量的綁定在聲明的時(shí)候就已經(jīng)確定,而不是在執(zhí)行的時(shí)候再根據(jù)上下文就近綁定。
上面的代碼中,foo 在聲明的時(shí)候就已經(jīng)把 a 綁定為 "before" 了。
這種特性也被稱(chēng)為閉包。或者說(shuō),閉包是實(shí)現(xiàn)詞法作用域的一種方式。
其實(shí)有些古老的語(yǔ)言的確采用動(dòng)態(tài)作用域 (dynamic scoping) (所謂動(dòng)態(tài)作用域就是在執(zhí)行的時(shí)候才確立變量綁定),比如 shell 腳本,emacslisp 等。
所以如果 JS 采用動(dòng)態(tài)作用域,那么上面的代碼將會(huì)輸出 "after"。
let 和 var
上面我們提到,JS 只有函數(shù)級(jí) (function level) 的作用域,這會(huì)導(dǎo)致什么問(wèn)題呢?看下面這段代碼。
<ul id="list">
</ul>
<script>
var list = document.getElementById("list");
for (var i = 1; i <= 5; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode("Item " + i));
item.onclick = function(ev) {
alert("Item " + i + " is clicked.");
};
list.appendChild(item);
}
</script>
(上述代碼的 JSFiddle 在線(xiàn)測(cè)試地址)
你會(huì)發(fā)現(xiàn)無(wú)論你點(diǎn)擊哪個(gè) Item,都只會(huì)顯示 6。
其是這是由于 i 是在 for 循環(huán)中定義的,而不是在函數(shù)中定義的,所以它是全局變量,循環(huán)完畢之后只有一個(gè) i ,其實(shí)此時(shí)所有的函數(shù)中的 i 都綁定到了那一個(gè) i 上。
上述代碼也可以簡(jiǎn)化為
var array = new Array();
for (var i = 1; i <= 5; i++) {
array[i] = function(){
return i;
}
}
array.forEach(function(fun){
console.log(fun());
})
為了解決這個(gè)問(wèn)題,ES6 中引入了 let 關(guān)鍵字,它有塊級(jí)(block level)作用域的性質(zhì)。所謂塊級(jí)作用域,也就是該變量的有效作用域以花括號(hào) {} 作為邊界。所以就不會(huì)提前到全局變量中了,而是以 for 循環(huán)的花括號(hào)作為邊界了。
把上面的 var 定義改為 let 或者用一個(gè)函數(shù)包裹起來(lái),那么點(diǎn)擊各個(gè) Item 就會(huì)出現(xiàn)不同的數(shù)字了。