先看下面一段代碼:
var a = 0;
alert("1st alert : a = " + a);
function fun(){
alert("2nd alert : a = " + a);
var a = 1;
setTimeout(function(){
alert("3rd alert : a = " + a);
a = 2;
},1000);
a = 3;
setTimeout(function(){
alert("4th alert : a = " + a);
a = 4;
},4000);
}
fun();
alert("5th alert : a = " + a);
代碼執行的結果是:
1st alert : a = 0
2nd alert : a = undefined
5th alert : a = 0
3rd alert : a = 3
4th alert : a = 2
疑問1:對於 2nd alert 而言,爲什麼 a 的值是 undefined ?
首先來看 JS 的執行環境和作用域。
執行環境(executing context)定義了變量或函數有權訪問的其他數據。在 JS 中,有兩種執行環境,一種是全局環境,也就是 Web 瀏覽器中的 window 對象,而另一種就是函數的執行環境。
在執行環境中存在一個變量對象,這個對象保存了環境中定義的所有變量和函數。
當代碼在執行的時候,會創建變量對象的一個作用域鏈,作用域鏈的前端是當前環境的變量對象,然後依次是外層環境的變量對象,逐層向外直到全局執行環境。
在標識符的解析過程中,會從前端開始沿着作用域鏈一級一級搜索,直到找到標識符或搜索完全局環境爲止。
所以在 JS 中,花括號並不代表一個獨立的作用域,循環體中定義的變量,循環體外依然可能訪問到(在同一個執行環境中)
看下面例子:
var a = 0;
function fun(){
alert("a=" + a);
}
fun();
結果是 a=0
這就是因爲在調用 fun 函數的時候,搜索標識符 a 並無法在本執行環境中找到,但是通過作用域鏈搜索到外層作用域的時候找到了 a
而爲什麼第一個例子中,2nd alert , a 的值是 undefined ?
這是因爲在 JS 中,使用 var 聲明的變量或者使用函數聲明方式聲明的函數(不是函數表達式)會自動被添加到最接近的環境中,也就是所謂的變量提升(hoisting)。什麼意思,相當於上面的 fun 函數定義中前兩行的代碼變成:
function fun(){
var a;
alert("2nd alert : a = " + a);
a = 1;
//other codes
}
所以在搜索標識符 a 的時候,在本執行環境中就可以搜索到,而不用搜索到外層環境,所以在 2nd alert 中,a 的值就是 undefined。
而對於函數的定義也是如此,這就是爲什麼使用函數聲明的方式時,調用函數可以放在函數聲明之前,而使用函數表達式的時候卻不可以的原因了。
疑問 2:5th alert 爲什麼在 3rd 和 4th 之前?
這是因爲 JavaScript 引擎對於其任務隊列是單線程處理的,而 setTimeout 屬於異步代碼,只有當 JS 線程中沒有同步代碼的時候纔會執行異步代碼。
setTimeout(function(){while(true){}},1000);
setTimeout(function(){alert('end 2');},2000);
setTimeout(function(){alert('end 1');},100);
alert('end');
所以在上面的代碼中,首先出現的是 end ,其次是 end 1,之後就再也不會出現。因爲在第一個 setTimeout 函數中死循環佔用了 JS 引擎的單線程,阻塞了其他進程。