說一道面試題,不要栽跟頭

題如下:

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 0);
    console.log(i);
}

結果是:0 1 2 3 3 3

很多公司面試都愛出這道題,此題考察的知識點還是蠻多的。
爲了防止初學者栽在此問題上,此文稍微分析一下。
都考察了那些知識點呢?
異步、作用域、閉包,你沒聽錯,是閉包。
我們來簡化此題:

setTimeout(function() {
    console.log(1);
}, 0);
console.log(2);

先打印2,後打印1。
因爲是 setTimeout 是異步的。
正確的理解 setTimeout 的方式:
有兩個參數,第一個參數是函數,第二參數是時間值。
調用setTimeout時,把函數參數,放到事件隊列中。等主程序運行完,再調用。
沒啥不好理解的。就像我們給按鈕綁定事件一樣:

btn.onclick = function() {
    alert(1);
};

這麼寫完,會彈出1嗎。不會!!只是綁定事件而已!
必須等我們去觸發事件,比如去點擊這個按鈕,纔會彈出1。
setTimeout也是這樣的!只是綁定事件,等主程序運行完畢後,再去調用。
setTimeout的時間值是怎麼回事呢?
比如:

setTimeout(fn, 2000)

我們可以理解爲2000之後,再放入事件隊列中,如果此時隊列爲空,那麼就直接調用 fn。如果前面還有其他的事件,那就等待。
因此 setTimeout 是一個約會從來都不準時的童鞋。
繼續看:

setTimeout(function() {
    console.log(i);
}, 0);
var i = 1;

程序會不會報錯?
不會!而且還會準確得打印1。
爲什麼?
因爲真正去執行 console.log(i) 這句代碼時,var i = 1 已經執行完畢了!
所以我們進行 dom 操作。可以先綁定事件,然後再去寫其他邏輯。

window.onload = function() {
    fn();
}
var fn = function() {
    alert('hello')
};

這麼寫,完全是可以的。因爲異步!

es5中是沒有塊級作用域的

for (var i = 0; i < 3; i++) {}
console.log(i);

也就說i可以在for循環體外訪問到。所以是沒有塊級作用域。
但此問題在es6裏終結了,因爲es6,發明了let。

這回我們再來看看原題。
原題使用了 for 循環。循環的本質是幹嘛的?
是爲了方便我們程序員,少寫重複代碼。
讓我們倒退50年,原題等價於:

var i = 0;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;

因爲 setTimeout 是註冊事件。根據前面的討論,可以都放在後面。
原題又等價於如下的寫法:

var i = 0;
console.log(i);
i++;
console.log(i);
i++;
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
setTimeout(function() {
    console.log(i);
}, 0);
setTimeout(function() {
    console.log(i);
}, 0);

這回你明白了爲啥結果是0 1 2 3 3 3了吧。

那個,你說它是閉包,又是怎麼回事?
爲了很好的說明白這個事情,我們把它放到一個函數中:

var fn = function() {
    for (var i = 0; i < 3; i++) {
        setTimeout(function() {
            console.log(i);
        }, 0);
        console.log(i);
    }
};
fn();

上面的函數跟我們常見另一個例子(div綁定事件)有什麼區別:

var fn = function() {
    var divs = document.querySelectorAll('div');
    for (var i = 0; i < 3; i++) {
        divs[i].onclick = function() {
            alert(i);
        };
    }
};
fn();

點擊每個div都會彈出3。道理是一樣的。因爲 alert(i) 中的 i 是 fn 作用越中的,因而這是閉包。
《javascript忍者祕籍》書裏把一個函數能調用全局變量,也稱閉包。
因爲作者認爲全局環境也可以想象成一個大的頂級函數。
怎麼保證能彈出 0,1,2 呢。
解決之道:以毒攻毒!
再創建個閉包!!

var fn = function() {
    var divs = document.querySelectorAll('div');
    for (var i = 0; i < 3; i++) {
        divs[i].onclick = (function(i) {
            return function() {
                alert(i);
            };
        })(i);
    }
};
fn();

或者如下的寫法:

var fn = function() {
    var divs = document.querySelectorAll('div');
    for (var i = 0; i < 3; i++) {
        (function(i) {
            divs[i].onclick = function() {
                alert(i);
            };
        })(i);
    }
};
fn();

因此原題如果也想 setTimeout 也彈出 0,1,2 的話,改成如下:

for (var i = 0; i < 3; i++) {
    setTimeout((function(i) {
        return function() {
            console.log(i);
        };
    })(i), 0);
    console.log(i);
}

如果你依然在編程的世界裏迷茫,不知道自己的未來規劃,可以加入前端學習進階內推交流羣685910553前端資料分享)。裏面可以與大神一起交流並走出迷茫。

新手可免費領取學習資料,看看前輩們是如何在編程的世界裏傲然前行不停更新最新的教程和學習方法(詳細的前端項目實戰教學視頻),

有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章