今天咱們來(lái)聊聊一個(gè)經(jīng)典的面試題,也是很多新手容易踩坑的問(wèn)題——在for循環(huán)中使用setTimeout。先看這段代碼:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
你以為它會(huì)輸出0,1,2,3,4?太天真了!實(shí)際輸出是五個(gè)5!這是為什么?又該如何解決?且聽(tīng)我慢慢道來(lái)~
一、為什么會(huì)這樣?——作用域與閉包的"陷阱"
這個(gè)現(xiàn)象背后隱藏著JavaScript的兩個(gè)重要特性:
- var沒(méi)有塊級(jí)作用域:在for循環(huán)中用var聲明的i實(shí)際上是函數(shù)作用域(或全局作用域)的
- 異步執(zhí)行:setTimeout的回調(diào)函數(shù)會(huì)在循環(huán)結(jié)束后才執(zhí)行
具體執(zhí)行過(guò)程是這樣的:
- for循環(huán)瞬間執(zhí)行完畢(同步代碼),i從0增加到5(當(dāng)i=5時(shí)循環(huán)停止)
- 1秒后,5個(gè)setTimeout回調(diào)開(kāi)始執(zhí)行
- 此時(shí)它們?cè)L問(wèn)的都是同一個(gè)i,而i的值已經(jīng)是5了
- 所以輸出了5個(gè)5
二、解決方案1:使用IIFE創(chuàng)建閉包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
原理:
- 立即執(zhí)行函數(shù)(IIFE)為每次循環(huán)創(chuàng)建一個(gè)新作用域
- 把當(dāng)前的i值作為參數(shù)j傳入并"凍結(jié)"住
- 每個(gè)setTimeout回調(diào)訪問(wèn)的都是自己閉包中的j
三、解決方案2:使用let塊級(jí)作用域(ES6推薦)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
這是最優(yōu)雅的解決方案!
- let有塊級(jí)作用域,每次循環(huán)都會(huì)創(chuàng)建一個(gè)新的i
- 相當(dāng)于自動(dòng)為我們創(chuàng)建了閉包
- 代碼簡(jiǎn)潔直觀,沒(méi)有魔法
四、解決方案3:利用setTimeout的第三個(gè)參數(shù)
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
小技巧:
- setTimeout可以接受多個(gè)參數(shù),第三個(gè)及以后的參數(shù)會(huì)作為回調(diào)函數(shù)的參數(shù)
- 相當(dāng)于瀏覽器幫我們做了參數(shù)綁定
五、解決方案4:用bind提前綁定參數(shù)
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}.bind(null, i), 1000);
}
原理:
- Function.prototype.bind可以提前綁定參數(shù)
- 第一個(gè)參數(shù)是this(這里不需要所以傳null)
- 后續(xù)參數(shù)會(huì)作為綁定函數(shù)的參數(shù)
六、深入理解:為什么let能解決問(wèn)題?
let在for循環(huán)中的行為很特殊:
- 每次迭代都會(huì)創(chuàng)建一個(gè)新的詞法環(huán)境(可以理解為新的作用域)
- 新的i會(huì)在這個(gè)環(huán)境中初始化,值為上一次迭代結(jié)束時(shí)的值
- 相當(dāng)于自動(dòng)為我們創(chuàng)建了閉包
可以近似理解為:
{
let i = 0;
setTimeout(function() { console.log(i); }, 1000);
}
{
let i = 1;
setTimeout(function() { console.log(i); }, 1000);
}
七、實(shí)際開(kāi)發(fā)中的建議
- 默認(rèn)使用let/const:告別var,擁抱塊級(jí)作用域
- 注意異步代碼的依賴(lài)關(guān)系:異步回調(diào)中使用循環(huán)變量時(shí)要特別小心
- 合理使用閉包:理解閉包的工作原理,但不要濫用
- 考慮代碼可讀性:有時(shí)候把異步邏輯提取成獨(dú)立函數(shù)會(huì)更清晰
八、舉一反三:類(lèi)似的陷阱
這種問(wèn)題不僅出現(xiàn)在setTimeout中,其他異步場(chǎng)景也會(huì)遇到:
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i);
});
}
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i);
});
}
九、總結(jié)
問(wèn)題根源:var的作用域 + 異步執(zhí)行時(shí)機(jī)
解決方案:
- IIFE創(chuàng)建閉包(傳統(tǒng)方式)
- 使用let(最推薦)
- 利用setTimeout第三個(gè)參數(shù)
- 使用bind綁定參數(shù)
最佳實(shí)踐:使用let/const避免這類(lèi)問(wèn)題
記住,在JavaScript中,同步代碼和異步代碼的執(zhí)行時(shí)機(jī)是需要特別關(guān)注的重點(diǎn)。理解閉包和作用域,就能輕松應(yīng)對(duì)這類(lèi)問(wèn)題。
轉(zhuǎn)自https://juejin.cn/post/7510587921788321832
該文章在 2025/6/4 11:48:19 編輯過(guò)