最近面試遇到一個題目,是有關setTimeout(fn,0)和閉包應用的,題目如下:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 0);
console.log(i);
}
我當時想都沒想,直接給面試官說,輸出0,1,2.結果可想而知_。
很多公司面試都愛出這道題,此題考察的知識點還是蠻多的。
為了防止初學者栽在此問題上,此文稍微分析一下。
都考察了那些知識點呢?
異步、作用域、閉包,你沒聽錯,是閉包。
我們來簡化此題:
setTimeout(function() {
console.log(1);
}, 0);
console.log(2);
先打印2,后打印1。
因為是setTimeout是異步的。
正確的理解setTimeout的方式(注冊事件):
有兩個參數(shù),第一個參數(shù)是函數(shù),第二參數(shù)是時間值。
調(diào)用setTimeout時,把函數(shù)參數(shù),放到事件隊列中。等主程序運行完,再調(diào)用。
沒啥不好理解的。就像我們給按鈕綁定事件一樣:
btn.onclick = function() {
alert(1);
};
這么寫完,會彈出1嗎。不會??!只是綁定事件而已!
必須等我們?nèi)ビ|發(fā)事件,比如去點擊這個按鈕,才會彈出1。
setTimeout也是這樣的!只是綁定事件,等主程序運行完畢后,再去調(diào)用。
setTimeout的時間值是怎么回事呢?
比如:
setTimeout(fn, 2000)
我們可以理解為2000之后,再放入事件隊列中,如果此時隊列為空,那么就直接調(diào)用fn。如果前面還有其他的事件,那就等待。
因此setTimeout是一個約會從來都不準時的童鞋。
繼續(xù)看:
setTimeout(function() {
console.log(i);
}, 0);
var i = 1;
程序會不會報錯?
不會!而且還會準確得打印1。
為什么?
因為真正去執(zhí)行console.log(i)這句代碼時,var i = 1已經(jīng)執(zhí)行完畢了!
所以我們進行dom操作。可以先綁定事件,然后再去寫其他邏輯。
window.onload = function() {
fn();
}
var fn = function() {
alert('hello')
};
這么寫,完全是可以的。因為異步!
es5中是沒有塊級作用域的
for (var i = 0; i < 3; i++) {}
console.log(i);
也就說i可以在for循環(huán)體外訪問到。所以是沒有塊級作用域。
但此問題在es6里終結了,因為es6,發(fā)明了let。
這回我們再來看看原題。
原題使用了for循環(huán)。循環(huán)的本質(zhì)是干嘛的?
是為了方便我們程序員,少寫重復代碼。
讓我們倒退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是注冊事件。根據(jù)前面的討論,可以都放在后面。
原題又等價于如下的寫法:
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了吧。
那個,說它是閉包,又是怎么回事?
為了很好的說明白這個事情,我們把它放到一個函數(shù)中:
var fn = function() {
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 0);
console.log(i);
}
};
fn();
上面的函數(shù)跟我們常見另一個例子(div綁定事件)有什么區(qū)別:
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忍者秘籍》書里把一個函數(shù)能調(diào)用全局變量,也稱閉包。
因為作者認為全局環(huán)境也可以想象成一個大的頂級函數(shù)。
怎么保證能彈出0,1, 2呢。
解決之道:以毒攻毒!
再創(chuàng)建個閉包!!
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);
}
總結如下:
歸根結底是要理解setTimeout(fn,0)的意思,是從下一個Event loop開始執(zhí)行,即是等當前所有腳本執(zhí)行完再運行,就是"盡可能早",還有就是閉包的運用。
如果想深入理解的話,推薦篇文章,看完你就可以完全掌握這方面的知識點了:
原文地址:極客教程 https://www.geekjc.com/post/58ca7d8d32129645947aec84