一道面試題引起的「血案」

es6以前,js的做用域只有兩種,全局做用域和函數做用域,沒有像C和java那樣的塊級做用域,因而對於學了C或者java這類語言的而後學習js的同窗來講,會遇到不少坑。js的這個特性致使了代碼的可閱讀性、維護性和容錯性都不太好。所以es6能夠用let來申明變量,這種方式申明的變量是隻能在塊做用域裏訪問,不能跨塊訪問,也不能跨函數訪問。那麼咱們在使用let的時候,真的就徹底知道它怎麼用了嗎?

引子
看到這樣的一個面試題java

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {
}

你們猜猜2333毫秒後輸出的結果是什麼?這裏就是「血案」現場了
A類同窗:2 ×
B類同窗: 0 √
我想A類同窗佔了大多數,包括我在內es6

clipboard.png

前期知識點

異步面試

js中的異步包含如下幾種:
一、定時器
二、事件處理函數
三、Promise
四、回調函數
js異步的存在是由於,js是單線程的,若是一些任務須要處理時間比較耗時,那麼下面的任務就會一直等這個任務執行完成才能繼續,好比一些IO任務,這樣就會致使執行效率低效,因此js的設計者意識到了這點,設計了異步執行任務,主線程沒必要等待異步任務完成才執行下去,這樣咱們就能夠把一些耗時的任務設計成異步任務,將其掛起,讓主線程處理完一些比較重要的任務(ui渲染等)後回頭再來執行掛起的異步任務。閉包

做用域鏈

js存在兩種類型的做用域,全局做用域和函數做用域。js執行的時候,會建立一個執行上下文(context),並將該執行上下文中的全部變量、函數和函數參數放入一個對象中AO/VO(變量對象|活動對象),而且會保存父級的AO/VO到[[scope]]屬性當中。而後在查找變量的時候,會從當前的AO/VO中查找變量,若是沒有,就往[[scope]]屬性父級VO/AO查找變量,一直到全局的VO中,這樣就造成了一個scope chain(做用域鏈)。通俗點來說,做用域鏈就是js在執行的時候用於搜索變量所在的一條鏈子,全部變量的獲取變量會順着這條鏈子往上查找,在本做用域內找不到變量的申明,就會往上一級的做用域中查找,直到在全局做用域中還找不到,就找不到該變量了。看下面的例子。異步

var outer = 1;
function func1() {
var inner1 = 2;
function func2() {
    var inner2 = 3
    console.log(inner2, inner1, outer); // 3 2 1
}
func2()
}

func1();

clipboard.png

一、首先獲取inner2,在func2的做用域中(活動對象)找到了inner2的申明,找到了,而且是3;
二、接着獲取inner1,發現func2的做用域中沒有inner1的申明,那麼往建立func2的做用域中查找,即func1中查找inner1的申明,而且爲2;
三、接着獲取outer,在func2中的做用域中找不到,往做用域鏈的上一級找,func1中也沒有outer的申明,那麼就繼續往上一級找,在全局做用域中找到了outer,因此是1。
接着咱們講下閉包,所謂閉包用一句話來講就是,函數中的函數,而且裏面的函數引用了外面的函數的變量。咱們瞭解了做用域鏈,那麼咱們就知道,函數內部是能夠訪問函數外部的變量的,因此,若是咱們在函數中的函數中有訪問函數外部變量,且該內部函數被返回的時候就造成了閉包。看下面例子:函數

function func() {
var name = 'liming';
var sayName = function() {
    console.log(name)
}
return sayName;
}

var sayName = func();
sayName(); // 輸出liming

如上面,就是閉包的一個例子,總結開來有兩個特色:

一、外部函數包含內部函數,且內部函數訪問的外部函數的變量
二、返回內部函數給外部調用
閉包有個缺陷就是容易致使內存泄漏,普通函數調用完後,js引擎就會銷燬函數裏面的變量,可是閉包的話就不會釋放了,因此須要注意點。學習

解析

選答案A的同窗ui

對於A類同窗,答案是錯的,可是能夠看出A類同窗對js的異步和閉包比較熟悉。咱們知道setTimeout裏面的函數是異步執行的,屬於js裏面的宏任務(js的異步任務分宏任務和微任務),須要等待js的主線程執行完畢且等到設置的時間後才從宏隊列裏面取出來執行。因此,等到setTimeout的回調執行的時候,回調函數要獲取i的值,這個時候回到函數裏面沒有i的定義,那麼js引擎就會往上一級做用域鏈中找i,這個時候就找到上一級做用域中的i,A類同窗以爲這個時候循環已經結束了(由於for是主線程),那麼這個時候的i應該是2了,因此輸出的應該++了兩次的2。這也就是閉包的知識點,js的設計是,內部能夠訪問外部,而外部不能夠訪問內部,因此在setTimeout中的回調中,它能夠訪問獲得外部的i,其實若是把let換成var的話,這個答案就是對的。spa

關於倒計時,這裏有個東西多說一句,就是setInterval的倒計時不是在回調執行完畢後纔開始的。這就會致使一種狀況,就是若是回調函數裏面執行的代碼時間比倒計時時間長,那麼下次插入隊列中的回調就會被取消,也就是倒計時到了之後,此次回調不會執行了,因此建議統一使用setTimeout來代替setInterval。

選擇B的同窗

選擇B的同窗,要不就是剛學習js的(也多是蒙對☺),要不就是對let知識點很熟悉的。線程

let關鍵字

let關鍵字申明的變量具備塊級做用域的做用,具備如下特色:
一、不可重複申明同個變量
二、不存在變量提高,因此必須先申明後使用
三、只有塊內可見,不會影響塊外的變量
其實let還有一個特色,就是在for循環當中,每輪循環都是一個新的值。看下面的的例子:

for(let i = 0; i < 2; i++) {
setTimeout(() => {
    console.log(i); // 分別輸出0和1
}, 0)
}

從這個例子能夠看出,let變量在for循環中,都會被從新賦值一個新的值,所以上面代碼中,for循環中獲取的i值都是一個新的,而且這個新i的值是上一次循環的i的值。相似這樣的僞代碼:

for(var i = 0; i < 2; i++) {
var new_i = i; // 新的i,且新的i應該是和真正的i關聯的,好比是new_i_0、new_i_1之類的,這段是僞代碼,用來講明,評論的同
學說,let的i是被挾持了,這個解釋很贊,因此for中的i其實都是被js引擎挾持了的i,不是咱們看到的i
setTimeout(() => {
    console.log(new_i); // 分別輸出0和1
}, 0)
}

我的以爲,這個是let的塊級做用域相關,每次循環的時候的i都是塊級做用域,只對本次循環可見,下次循環不可見。
因此,咱們之後若是須要再for循環中獲取循環項的時候,能夠不用當即執行函數來實現了,能夠改成let了。
回到正題。for循環的第一個語句是初始化,這個時候的i就是本來的i,初始化爲0,後面的i都是每次循環新生成的i,與初始化的i無關,因此到2333毫秒之後,i的值任然爲0,所以打印出來的i就是0了。

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {

}

總結

本篇文章經過一個特殊的面試題,引出了js的異步、做用域鏈、閉包和let的知識點。異步包含:一、定時器二、事件處理函數三、Promise四、回調函數異步函數的執行時須要主線程空閒的時候執行的,因此咱們會把耗時的任務處理爲異步。做用域鏈:每一個函數執行的時候都會建立一個做用域鏈的對象,它包含了函數內的全部變量以及建立該函數的函數的全部變量,一直到全局變量,訪問變量的時候就會沿着這條鏈子找。閉包:一、外部函數包含內部函數,且內部函數訪問的外部函數的變量二、返回內部函數給外部調用let:一、不可重複申明同個變量二、不存在變量提高,因此必須先申明後使用三、只有塊內可見,不會影響塊外的變量還有在for循環中,每次循環獲取let聲明的變量都是一個新的變量,而不是初始化時候的那個變量。

相關文章
相關標籤/搜索