寫(xiě)在前面翅娶,筆者在做面試官這 2 年多的時(shí)間內(nèi),面試了數(shù)百個(gè)前端工程師好唯,驚訝的發(fā)現(xiàn)竭沫,超過(guò) 80% 的候選人對(duì)下面這道題的回答情況連及格都達(dá)不到。這究竟是怎樣神奇的一道面試題骑篙?他考察了候選人的哪些能力蜕提?對(duì)正在讀本文的你有什么啟示?且聽(tīng)我慢慢道來(lái)
不起眼的開(kāi)始
招聘前端工程師靶端,尤其是中高級(jí)前端工程師谎势,扎實(shí)的 JS 基礎(chǔ)絕對(duì)是必要條件,基礎(chǔ)不扎實(shí)的工程師在面對(duì)前端開(kāi)發(fā)中的各種問(wèn)題時(shí)大概率會(huì)束手無(wú)策躲查。在考察候選人 JS 基礎(chǔ)的時(shí)候它浅,我經(jīng)常會(huì)提供下面這段代碼,然后讓候選人分析它實(shí)際運(yùn)行的結(jié)果:
<pre lang="js" class="">
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
</pre>
這段代碼很短镣煮,只有 7 行姐霍,我想,能讀到這里的同學(xué)應(yīng)該不需要我逐行解釋這段代碼在做什么吧。候選人面對(duì)這段代碼時(shí)給出的結(jié)果也不盡相同镊折,以下是典型的答案:
- A. 20% 的人會(huì)快速掃描代碼胯府,然后給出結(jié)果:
0,1,2,3,4,5
; - B. 30% 的人會(huì)拿著代碼逐行看恨胚,然后給出結(jié)果:
5,0,1,2,3,4
骂因; - C. 50% 的人會(huì)拿著代碼仔細(xì)琢磨,然后給出結(jié)果:
5,5,5,5,5,5
赃泡;
只要你對(duì) JS 中同步和異步代碼的區(qū)別寒波、變量作用域、閉包等概念有正確的理解升熊,就知道正確答案是 C俄烁,代碼的實(shí)際輸出是:
<pre lang="bash" class="">
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
</pre>
接下來(lái)我會(huì)追問(wèn):如果我們約定,用箭頭表示其前后的兩次輸出之間有 1 秒的時(shí)間間隔级野,而逗號(hào)表示其前后的兩次輸出之間的時(shí)間間隔可以忽略页屠,代碼實(shí)際運(yùn)行的結(jié)果該如何描述?會(huì)有下面兩種答案:
- A. 60% 的人會(huì)描述為:
5 -> 5 -> 5 -> 5 -> 5
蓖柔,即每個(gè) 5 之間都有 1 秒的時(shí)間間隔辰企; - B. 40% 的人會(huì)描述為:
5 -> 5,5,5,5,5
,即第 1 個(gè) 5 直接輸出况鸣,1 秒之后牢贸,輸出 5 個(gè) 5;
這就要求候選人對(duì) JS 中的定時(shí)器工作機(jī)制非常熟悉懒闷,循環(huán)執(zhí)行過(guò)程中十减,幾乎同時(shí)設(shè)置了 5 個(gè)定時(shí)器,一般情況下愤估,這些定時(shí)器都會(huì)在 1 秒之后觸發(fā),而循環(huán)完的輸出是立即執(zhí)行的速址,顯而易見(jiàn)玩焰,正確的描述是 B。
如果到這里算是及格的話(huà)芍锚,100 個(gè)人參加面試只有 20 人能及格昔园,讀到這里的同學(xué)可以仔細(xì)思考,你及格了么并炮?
追問(wèn) 1:閉包
如果這道題僅僅是考察候選人對(duì) JS 異步代碼默刚、變量作用域的理解,局限性未免太大逃魄,接下來(lái)我會(huì)追問(wèn)荤西,如果期望代碼的輸出變成:5 -> 0,1,2,3,4
,該怎么改造代碼?熟悉閉包的同學(xué)很快能給出下面的解決辦法:
<pre lang="js" class="">
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);
</pre>
巧妙的利用 IIFE(Immediately Invoked Function Expression:聲明即執(zhí)行的函數(shù)表達(dá)式)來(lái)解決閉包造成的問(wèn)題邪锌,確實(shí)是不錯(cuò)的思路勉躺,但是初學(xué)者可能并不覺(jué)得這樣的代碼很好懂,至少筆者初入門(mén)的時(shí)候這里琢磨了一會(huì)兒才真正理解觅丰。
有沒(méi)有更符合直覺(jué)的做法饵溅?答案是有,我們只需要對(duì)循環(huán)體稍做手腳妇萄,讓負(fù)責(zé)輸出的那段代碼能拿到每次循環(huán)的 i 值即可蜕企。該怎么做呢?利用 JS 中基本類(lèi)型(Primitive Type)的參數(shù)傳遞是按值傳遞(Pass by Value)的特征冠句,不難改造出下面的代碼:
</pre>
能給出上述 2 種解決方案的候選人可以認(rèn)為對(duì) JS 基礎(chǔ)的理解和運(yùn)用是不錯(cuò)的糖赔,可以各加 10 分。當(dāng)然實(shí)際面試中還有候選人給出如下的代碼:
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)轩端,這里只有個(gè)非常細(xì)微的變動(dòng)放典,即使用 ES6 塊級(jí)作用域(Block Scope)中的 let 替代了 var,但是代碼在實(shí)際運(yùn)行時(shí)會(huì)報(bào)錯(cuò)基茵,因?yàn)樽詈竽莻€(gè)輸出使用的 i 在其所在的作用域中并不存在奋构,i 只存在于循環(huán)內(nèi)部。
能想到 ES6 特性的同學(xué)雖然沒(méi)有答對(duì)拱层,但是展示了自己對(duì) ES6 的了解弥臼,可以加 5 分,繼續(xù)進(jìn)行下面的追問(wèn)根灯。
追問(wèn) 2:ES6
有經(jīng)驗(yàn)的前端同學(xué)讀到這里可能有些不耐煩了径缅,扯了這么多,都是他知道的內(nèi)容烙肺,先別著急纳猪,挑戰(zhàn)的難度會(huì)繼續(xù)增加。
接著上文繼續(xù)追問(wèn):如果期望代碼的輸出變成 0 -> 1 -> 2 -> 3 -> 4 -> 5
桃笙,并且要求原有的代碼塊中的循環(huán)和兩處 console.log 不變氏堤,該怎么改造代碼?新的需求可以精確的描述為:代碼執(zhí)行時(shí)搏明,立即輸出 0鼠锈,之后每隔 1 秒依次輸出 1,2,3,4,循環(huán)結(jié)束后在大概第 5 秒的時(shí)候輸出 5(這里使用大概星著,是為了避免鉆牛角尖的同學(xué)陷進(jìn)去购笆,因?yàn)?JS 中的定時(shí)器觸發(fā)時(shí)機(jī)有可能是不確定的,具體可參見(jiàn) How Javascript Timers Work)虚循。
看到這里同欠,部分同學(xué)會(huì)給出下面的可行解:
不得不承認(rèn)样傍,這種做法雖粗暴有效,但是不算是能額外加分的方案行您。如果把這次的需求抽象為:在系列異步操作完成(每次循環(huán)都產(chǎn)生了 1 個(gè)異步操作)之后铭乾,再做其他的事情,代碼該怎么組織娃循?聰明的你是不是想起了什么炕檩?對(duì),就是 Promise捌斧。
可能有的同學(xué)會(huì)問(wèn)笛质,不就是在控制臺(tái)輸出幾個(gè)數(shù)字么?至于這樣殺雞用牛刀捞蚂?你要知道妇押,面試官真正想考察的是候選人是否具備某種能力和素質(zhì),因?yàn)樵诂F(xiàn)代的前端開(kāi)發(fā)中姓迅,處理異步的代碼隨處可見(jiàn)敲霍,熟悉和掌握異步操作的流程控制是成為合格開(kāi)發(fā)者的基本功。
順著下來(lái)丁存,不難給出基于 Promise 的解決方案(既然 Promise 是 ES6 中的新特性肩杈,我們的新代碼使用 ES6 編寫(xiě)是不是會(huì)更好?如果你這么寫(xiě)了解寝,大概率會(huì)讓面試官心生好感):
相比而言扩然,筆者更傾向于下面這樣看起來(lái)更簡(jiǎn)潔的代碼,要知道編程風(fēng)格也是很多面試官重點(diǎn)考察的點(diǎn)聋伦,代碼閱讀時(shí)的顆粒度更小夫偶,模塊化更好,無(wú)疑會(huì)是加分點(diǎn)觉增。
<pre lang="js" class="">
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);
});
</pre>
讀到這里的同學(xué),恭喜你抑片,你下次面試遇到類(lèi)似的問(wèn)題卵佛,至少能拿到 80 分。
我們都知道使用 Promise 處理異步代碼比回調(diào)機(jī)制讓代碼可讀性更高敞斋,但是使用 Promise 的問(wèn)題也很明顯,即如果沒(méi)有處理 Promise 的 reject疾牲,會(huì)導(dǎo)致錯(cuò)誤被丟進(jìn)黑洞植捎,好在新版的 Chrome 和 Node 7.x 能對(duì)未處理的異常給出 Unhandled Rejection Warning,而排查這些錯(cuò)誤還需要一些特別的技巧(瀏覽器阳柔、Node.js)焰枢。
追問(wèn) 3:ES7
既然你都看到這里了,那就再堅(jiān)持 2 分鐘,接下來(lái)的內(nèi)容會(huì)讓你明白你的堅(jiān)持是值得的济锄。
多數(shù)面試官在決定聘用某個(gè)候選人之前還需要考察另外一項(xiàng)重要能力暑椰,即技術(shù)自驅(qū)力,直白的說(shuō)就是候選人像有內(nèi)部的馬達(dá)在驅(qū)動(dòng)他荐绝,用漂亮的方式解決工程領(lǐng)域的問(wèn)題一汽,不斷的跟隨業(yè)務(wù)和技術(shù)變得越來(lái)越牛逼,究竟什么是牛逼低滩?建議閱讀程序人生的這篇剖析召夹。
回到正題,既然 Promise 已經(jīng)被拿下恕沫,如何使用 ES7 中的 async await 特性來(lái)讓這段代碼變的更簡(jiǎn)潔监憎?你是否能夠根據(jù)自己目前掌握的知識(shí)給出答案?請(qǐng)?jiān)谶@里暫停 1 分鐘婶溯,思考下鲸阔。
下面是筆者給出的參考代碼:
總結(jié)
感謝你花時(shí)間讀到這里,相信你收獲的不僅僅是用 JS 精確控制代碼輸出的各種技巧迄委,更是對(duì)于前端工程師的成長(zhǎng)期許:扎實(shí)的語(yǔ)言基礎(chǔ)褐筛、與時(shí)俱進(jìn)的能力、強(qiáng)大技術(shù)自驅(qū)力跑筝。