淺析關(guān)于 JS 作用域的幾個(gè)高頻知識(shí)點(diǎn)

閉包 詞法作用域 變量提升


變量提升

什么是變量提升

顧名思義,變量提升指的是,在聲明變量的時(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_oddis_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ù)字了。


參考鏈接

編程語(yǔ)言中的變量作用域與閉包

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容