80%應聘者都不及格的JS面試題

不起眼的開始javascript

招聘前端工程師,尤爲是中高級前端工程師,紮實的 JS 基礎絕對是必要條件,基礎不紮實的工程師在面對前端開發中的各類問題時大機率會一籌莫展。在考察候選人 JS 基礎的時候,我常常會提供下面這段代碼,而後讓候選人分析它實際運行的結果:html

1前端

2java

3node

4面試

5編程

6api

7promise

for (var i = 0; i < 5; i++) {瀏覽器

 setTimeout(function() {

  console.log(new Date, i);

 }, 1000);

}

 

console.log(new Date, i);

這段代碼很短,只有 7 行,我想,能讀到這裏的同窗應該不須要我逐行解釋這段代碼在作什麼吧。候選人面對這段代碼時給出的結果也不盡相同,如下是典型的答案:

A. 20% 的人會快速掃描代碼,而後給出結果:0,1,2,3,4,5;
B. 30% 的人會拿着代碼逐行看,而後給出結果:5,0,1,2,3,4;
C. 50% 的人會拿着代碼仔細琢磨,而後給出結果:5,5,5,5,5,5;

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

1

2

3

4

5

6

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 秒的時間間隔,而逗號表示其先後的兩次輸出之間的時間間隔能夠忽略,代碼實際運行的結果該如何描述?會有下面兩種答案:

A. 60% 的人會描述爲:5 -> 5 -> 5 -> 5 -> 5,即每一個 5 之間都有 1 秒的時間間隔;
B. 40% 的人會描述爲:5 -> 5,5,5,5,5,即第 1 個 5 直接輸出,1 秒以後,輸出 5 個 5;

這就要求候選人對 JS 中的定時器工做機制很是熟悉,循環執行過程當中,幾乎同時設置了 5 個定時器,通常狀況下,這些定時器都會在 1 秒以後觸發,而循環完的輸出是當即執行的,顯而易見,正確的描述是 B。

若是到這裏算是及格的話,100 我的參加面試只有 20 人能及格,讀到這裏的同窗能夠仔細思考,你及格了麼?

追問 1:閉包

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

1

2

3

4

5

6

7

8

9

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);

巧妙的利用 IIFE(Immediately Invoked Function Expression:聲明即執行的函數表達式)來解決閉包形成的問題,確實是不錯的思路,可是初學者可能並不以爲這樣的代碼很好懂,至少筆者初入門的時候這裏琢磨了一下子才真正理解。

有沒有更符合直覺的作法?答案是有,咱們只須要對循環體稍作手腳,讓負責輸出的那段代碼能拿到每次循環的 i 值便可。該怎麼作呢?利用 JS 中基本類型(Primitive Type)的參數傳遞是按值傳遞(Pass by Value)的特徵,不難改造出下面的代碼:

1

2

3

4

5

6

7

8

9

10

11

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 分。固然實際面試中還有候選人給出以下的代碼:

1

2

3

4

5

6

7

for (let i = 0; i < 5; i++) {

 setTimeout(function() {

  console.log(new Date, i);

 }, 1000);

}

 

console.log(new Date, i);

細心的同窗會發現,這裏只有個很是細微的變更,即便用 ES6 塊級做用域(Block Scope)中的 let 替代了 var,可是代碼在實際運行時會報錯,由於最後那個輸出使用的 i 在其所在的做用域中並不存在,i 只存在於循環內部。

能想到 ES6 特性的同窗雖然沒有答對,可是展現了本身對 ES6 的瞭解,能夠加 5 分,繼續進行下面的追問。

追問 2:ES6

有經驗的前端同窗讀到這裏可能有些不耐煩了,扯了這麼多,都是他知道的內容,先彆着急,挑戰的難度會繼續增長。

接着上文繼續追問:若是指望代碼的輸出變成 0 -> 1 -> 2 -> 3 -> 4 -> 5,而且要求原有的代碼塊中的循環和兩處 console.log 不變,該怎麼改造代碼?新的需求能夠精確的描述爲:代碼執行時,當即輸出 0,以後每隔 1 秒依次輸出 1,2,3,4,循環結束後在大概第 5 秒的時候輸出 5(這裏使用大概,是爲了不鑽牛角尖的同窗陷進去,由於 JS 中的定時器觸發時機有多是不肯定的,具體可參見 How Javascript Timers Work)。

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

1

2

3

4

5

6

7

8

9

10

11

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);

不得不認可,這種作法雖粗暴有效,可是不算是能額外加分的方案。若是把此次的需求抽象爲:在系列異步操做完成(每次循環都產生了 1 個異步操做)以後,再作其餘的事情,代碼該怎麼組織?聰明的你是否是想起了什麼?對,就是 Promise

可能有的同窗會問,不就是在控制檯輸出幾個數字麼?至於這樣殺雞用牛刀?你要知道,面試官真正想考察的是候選人是否具有某種能力和素質,由於在現代的前端開發中,處理異步的代碼隨處可見,熟悉和掌握異步操做的流程控制是成爲合格開發者的基本功。

順着下來,不難給出基於 Promise 的解決方案(既然 Promise 是 ES6 中的新特性,咱們的新代碼使用 ES6 編寫是否是會更好?若是你這麼寫了,大機率會讓面試官心生好感):

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

const tasks = [];

for (var i = 0; i < 5; i++) { // 這裏 i 的聲明不能改爲 let,若是要改該怎麼作?

 ((j) => {

  tasks.push(new Promise((resolve) => {

   setTimeout(() => {

    console.log(new Date, j);

    resolve(); // 這裏必定要 resolve,不然代碼不會按預期 work

   }, 1000 * j); // 定時器的超時時間逐步增長

  }));

 })(i);

}

 

Promise.all(tasks).then(() => {

 setTimeout(() => {

  console.log(new Date, i);

 }, 1000); // 注意這裏只須要把超時設置爲 1 秒

});

相比而言,筆者更傾向於下面這樣看起來更簡潔的代碼,要知道編程風格也是不少面試官重點考察的點,代碼閱讀時的顆粒度更小,模塊化更好,無疑會是加分點。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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);

});

讀到這裏的同窗,恭喜你,你下次面試遇到相似的問題,至少能拿到 80 分。

咱們都知道使用 Promise 處理異步代碼比回調機制讓代碼可讀性更高,可是使用 Promise 的問題也很明顯,即若是沒有處理 Promise 的 reject,會致使錯誤被丟進黑洞,好在新版的 Chrome 和 Node 7.x 能對未處理的異常給出 Unhandled Rejection Warning,而排查這些錯誤還須要一些特別的技巧(瀏覽器Node.js)。

追問 3:ES7

既然你都看到這裏了,那就再堅持 2 分鐘,接下來的內容會讓你明白你的堅持是值得的。

多數面試官在決定聘用某個候選人以前還須要考察另一項重要能力,即技術自驅力,直白的說就是候選人像有內部的馬達在驅動他,用漂亮的方式解決工程領域的問題,不斷的跟隨業務和技術變得愈來愈牛逼,究竟什麼是牛逼?建議閱讀程序人生的這篇剖析

回到正題,既然 Promise 已經被拿下,如何使用 ES7 中的 async await 特性來讓這段代碼變的更簡潔?你是否可以根據本身目前掌握的知識給出答案?請在這裏暫停 1 分鐘,思考下。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// 模擬其餘語言中的 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 精確控制代碼輸出的各類技巧,更是對於前端工程師的成長期許:紮實的語言基礎、與時俱進的能力、強大技術自驅力。

 

ps:

setTimeout(function(){console.log(1)},0); new Promise(function(resolve){     console.log(2)     for( var i=0 ; i<10000 ; i++ ){         i==9999 && resolve()     }     console.log(3) }).then(function(){     console.log(4) }); console.log(5); //   這的問題是,爲何答案是 2 3 5 4 1 //   而不是 2 3 5 1 4 這個問題,我是這麼理解的,瀏覽器讀取script標籤中的代碼也是一個事件隊列,Promise的任務會在當前事件循環末尾中執行,而setTimeout中的任務是在下一次事件循環執行

相關文章
相關標籤/搜索