破解前端面試(80% 應聘者不及格系列):從 閉包說起

小時。發表於2019-01-06

不起眼的開始

招聘前端工程師,尤其是中高階前端工程師,紮實的 JS 基礎絕對是必要條件,基礎不紮實的工程師在面對前端開發中的各種問題時大概率會束手無策。在考察候選人 JS 基礎的時候,我經常會提供下面這段程式碼,然後讓候選人分析它實際執行的結果:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}

console.log(new Date, i);
複製程式碼

只要你對 JS 中同步和非同步程式碼的區別、變數作用域、閉包等概念有正確的理解,就知道正確答案是 C,程式碼的實際輸出是:

2017-03-18T00:43:45.873Z 5
2017-03-18T00:43:46.866Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
2017-03-18T00:43:46.868Z 5
複製程式碼

追問 1:閉包

如果這道題僅僅是考察候選人對 JS 非同步程式碼、變數作用域的理解,侷限性未免太大,接下來我會追問,如果期望程式碼的輸出變成:5 -> 0,1,2,3,4,該怎麼改造程式碼?熟悉閉包的同學很快能給出下面的解決辦法:

第一種方法

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);
複製程式碼

第二種方法

var output = function (i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    output(i);  // 這裡傳過去的 i 值被複制了
}

console.log(new Date, i);
複製程式碼

能給出上述 2 種解決方案的候選人可以認為對 JS 基礎的理解和運用是不錯的,可以各加 10 分。當然實際面試中還有候選人給出如下的程式碼:

第三種方法

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}
var i = 5;
console.log(new Date, i);
複製程式碼

追問 2:ES6

如果期望程式碼的輸出變成 0 -> 1 -> 2 -> 3 -> 4 -> 5,並且要求原有的程式碼塊中的迴圈和兩處 console.log 不變,該怎麼改造程式碼?新的需求可以精確的描述為:程式碼執行時,立即輸出 0,之後每隔 1 秒依次輸出 1,2,3,4,迴圈結束後在大概第 5 秒的時候輸出 5(這裡使用大概,是為了避免鑽牛角尖的同學陷進去,因為 JS 中的定時器觸發時機有可能是不確定的,具體可參見 How Javascript Timers Work)。

看到這裡,部分同學會給出下面的可行解:

for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000 * j));  // 這裡修改 0~4 的定時器時間
    })(i);
}

setTimeout(function() { // 這裡增加定時器,超時設定為 5 秒
    console.log(new Date, i);
}, 1000 * i);
複製程式碼

順著下來,不難給出基於 Promise 的解決方案(既然 Promise 是 ES6 中的新特性,我們的新程式碼使用 ES6 編寫是不是會更好?如果你這麼寫了,大概率會讓面試官心生好感):

const tasks = []; // 這裡存放非同步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成全部的非同步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 非同步操作完成之後,輸出最後的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});
複製程式碼

追問 3:ES7

既然 Promise 已經被拿下,如何使用 ES7 中的 async await 特性來讓這段程式碼變的更簡潔?你是否能夠根據自己目前掌握的知識給出答案?請在這裡暫停 1 分鐘,思考下。

下面是筆者給出的參考程式碼:

// 模擬其他語言中的 sleep,實際上可以是任何非同步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 宣告即執行的 async 函式表示式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
複製程式碼

總結

感謝你花時間讀到這裡,相信你收穫的不僅僅是用 JS 精確控制程式碼輸出的各種技巧,更是對於前端工程師的成長期許:紮實的語言基礎、與時俱進的能力、強大技術自驅力。

相關文章