【歡迎讀者將自己的疑問以評論的形式反饋給我,我會逐一解答 :)】
從我上一篇文章《5個典型的JavaScript面試題》的統計資料來看,你們中似乎有很多人正在尋找工作,或者至少想檢測一下自己的JavaScript知識。不管是什麼原因讓你看到這篇文章,我決定再寫一篇,介紹一下另外幾個典型的面試題,這也是我答應過JavaScript頻道編輯Colin Ihrig的一件事情。
問題1:閉包
考慮下面的程式碼:
1 2 3 4 5 6 |
var nodes = document.getElementsByTagName('button'); for (var i = 0; i < nodes.length; i++) { nodes[i].addEventListener('click', function() { console.log('You clicked element #' + i); }); } |
請問,如果使用者點選第一個和第四個按鈕,控制檯上會輸出什麼?為什麼?
答案
上面程式碼的目的在於檢測JavaScript的一個重要概念:閉包。對於每一個JavaScript開發者來說,如果你想在網頁中編寫5行以上的程式碼,那麼準確理解和恰當使用閉包是非常重要的。如果你想開始學習或者只是想簡單地溫習一下閉包,那麼我強烈建議你去閱讀這個教程:Colin Ihrig 寫的JavaScript Closures Demystified 。
好了,回到上面的程式碼。控制檯會輸出兩次You clicked element #NODES_LENGTH,其中#NODES_LENGTH等於nodes的結點個數。由於閉包中變數的值不是靜態的,i的值並不是新增click事件處理器時的值(比如,當給第一個button新增click事件處理器時i為0,給第二個新增時i為1)。當for迴圈結束時,變數i的值等於nodes的長度。因此事件被執行時,控制檯會輸出變數i當前的值,即等於nodes的長度。
問題2:閉包
修復上題的問題,使得點選第一個按鈕時輸出0,點選第二個按鈕時輸出1。
答案
有多種辦法解決這個問題,下面我給出其中的兩種。
第一個解決方案要用到一個IIFE來建立另外一個閉包,從而得到所希望的i的值。相應的程式碼如下:
1 2 3 4 5 6 7 8 |
var nodes = document.getElementsByTagName('button'); for (var i = 0; i < nodes.length; i++) { nodes[i].addEventListener('click', (function(i) { return function() { console.log('You clicked element #' + i); } })(i)); } |
另一個解決方案不使用IIFE,而是將函式移到迴圈的外面,程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
function handlerWrapper(i) { return function() { console.log('You clicked element #' + i); } } var nodes = document.getElementsByTagName('button'); for (var i = 0; i <nodes.length; i++) { nodes[i].addEventListener('click', handlerWrapper(i)); } |
問題3:資料型別
考慮如下程式碼:
1 2 3 4 |
console.log(typeof null); console.log(typeof {}); console.log(typeof []); console.log(typeof undefined); |
答案
這一個問題看起來似乎有點傻,但是它測試了typeof 操作符的知識。很多JavaScript開發者並沒有意識到typeof的獨特性。在本例中,控制檯會輸出下面的內容:
1 2 3 4 |
object object object undefined |
最讓人吃驚的輸出結果可能是第三個,許多開發者認為typeof [ ] 會返回Array。如果你想測試變數是否為陣列,可以寫下面的程式碼:
1 2 3 4 5 6 7 |
var myArray = []; if (myArray instanceof Array) { console.log("myArray is an instance of Array."); } else{ console.log("myArray is not an instance of Array."); } |
問題4:事件迴圈
下面程式碼執行結果是什麼?請解釋。
1 2 3 4 5 6 7 |
function printing() { console.log(1); setTimeout(function() { console.log(2); }, 1000); setTimeout(function() { console.log(3); }, 0); console.log(4); } printing(); |
答案
輸出結果:
1 2 3 4 |
1 4 3 2 |
要弄懂數字為何以這種順序輸出,你需要弄明白setTimeout()是幹什麼的,以及瀏覽器的事件迴圈工作原理。瀏覽器有一個事件迴圈用於檢查事件佇列,處理延遲的事件。UI事件(例如,點選,滾動等),Ajax回撥,以及提供給setTimeout()和setInterval()的回撥都會依次被事件迴圈處理。因此,當呼叫setTimeout()函式時,即使延遲的時間被設定為0,提供的回撥也會被排隊。回撥會呆在佇列中,直到指定的時間用完後,引擎開始執行動作(如果它在當前不執行其他的動作)。因此,即使setTimeout()回撥被延遲0毫秒,它仍然會被排隊,並且直到函式中其他非延遲的語句被執行完了之後,才會執行。
有了這些認識,理解輸出結果為“1”就容易了,因為它是函式的第一句並且沒有使用setTimeout()函式來延遲。接著輸出“4”,因為它是沒有被延遲的數字,也沒有進行排隊。然後,剩下了“2”,“3”,兩者都被排隊,但是前者需要等待一秒,後者等待0秒(這意味著引擎完成前兩個輸出之後馬上進行)。這就解釋了為什麼“3”在“2”之前。
【譯者推薦擴充套件閱讀:阮一峰文章JavaScript 執行機制詳解:再談Event Loop 】
問題5:演算法
寫一個判斷質數的isPrime()函式,當其為質數時返回true,否則返回false。
答案
我認為這是在面試中最常問到的一個問題。儘管這個問題反覆出現並且也很簡單,但是從候選人提供的答案中能很好地看出候選人的數學和演算法能力水平。
首先, 因為JavaScript不同於C或者Java,因此你不能信任傳遞來的資料型別。如果面試官沒有明確地告訴你,你應該詢問他是否需要做輸入檢查,還是不進行檢查直接寫函式。嚴格上說,應該對函式的輸入進行檢查。
需要記住的第二點,負數不是質數。同樣的,1和0都不是,因此,要對這些數字做檢測。另外,2是唯一的既是偶數又是質數的數字。沒有必要用一個迴圈來驗證4,6,8。再者,如果一個數字不能被2整除,它同樣也不能被4,6,8等整除,因此你的迴圈需要跳過這些數字。可以採取其他一些更明智的優化手段,我這裡採用的是適用於大多數情況的。例如,如果一個數字不能被5整除,它也不會被5的倍數整除。所以,沒有必要檢測10,15,20等等。如果你深入瞭解這個問題的解決方案,我建議你去看相關的Wikipedia介紹。
最後一點,你不需要檢查比輸入數字的開方還要大的數字。我感覺人們會遺漏掉這一點,並且也不會因為此而獲得消極的反饋。但是,展示出這一方面的知識會給你額外加分。
現在你具備了這個問題的背景知識,下面是總結以上所有考慮的解決方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function isPrime(number) { // If your browser doesn't support the method Number.isInteger of ECMAScript 6, // you can implement your own pretty easily if (typeof number !== 'number' || !Number.isInteger(number)) { // Alternatively you can throw an error. return false; } if (number < 2) { return false; } if (number === 2) { return true; } else if (number % 2 === 0) { return false; } var squareRoot = Math.sqrt(number); for(var i = 3; i <= squareRoot; i += 2) { if (number % i === 0) { return false; } } return true; } |
結論
本文以問題和練習的形式討論了另外幾個重要的Javascript概念,這些都是前端開發者面試的重要內容。我希望你能順利地回答這些問題,或者從這裡學到一些新的東西,以便在下一次面試中有更好的表現。