10道典型的JavaScript面試題
問題1: 作用域(Scope)
考慮以下代碼:
(function() {
var a = b = 5;
})();
console.log(b);
上述代碼會打印出5
。這個問題的陷阱就是,在立即執行函數表達式(IIFE)中,有兩個賦值,但是其中變量a
使用關鍵詞var
來聲明。這就意味著a
是這個函數的局部變量。與此相反,b
被分配給了全局作用域(譯註:也就是全局變量)。
這個問題另一個陷阱就是,在函數中沒有使用”嚴格模式” (‘use strict‘;
)。如果 嚴格模式開啟,那麽代碼就會報錯 ” Uncaught ReferenceError: b is not defined” 。請記住,如果這是預期的行為,嚴格模式要求你顯式地引用全局作用域。所以,你需要像下面這麽寫:
(function() {
‘use strict‘;
var a = window.b = 5;
})();
console.log(b);
問題2: 創建 “原生(native)” 方法
在 String
對象上定義一個 repeatify
函數。這個函數接受一個整數參數,來明確字符串需要重復幾次。這個函數要求字符串重復指定的次數。舉個例子:
console.log(‘hello‘.repeatify(3));
應該打印出hellohellohello,
一個可行的做法如下:
String.prototype.repeatify = String.prototype.repeatify || function(times) {
var str = ‘‘;
for (var i = 0; i < times; i++) {
str += this;
}
return str;
};
這個問題測試了開發人員對 javascript 中繼承及原型(prototype
)屬性的知識。這也驗證了開發人員是否有能力擴展原生數據類型功能(雖然不應該這麽做)。在這裏,另一個關鍵點是,看你怎樣避免重寫可能已經定義了的方法。這可以通過在定義自己的方法之前,檢測方法是否已經存在。
String.prototype.repeatify = String.prototype.repeatify || function(times) {/* code here */};
當你去擴展一個Javascript方法時,這個技術非常有用。
問題3: 變量提升(Hoisting)
執行以下代碼的結果是什麽?為什麽?
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
這段代碼的執行結果是undefined
和 2
。
這個結果的原因是,變量和函數都被提升(hoisted) 到了函數體的頂部。因此,當打印變量a
時,它雖存在於函數體(因為a
已經被聲明),但仍然是undefined
。換言之,上面的代碼等同於下面的代碼:
function test() {
var a;
function foo() {
return 2;
}
console.log(a);
console.log(foo());
a = 1;
}
test();
問題4: 在javascript中,`this`是如何工作的
以下代碼的結果是什麽?請解釋你的答案。
var fullname = ‘John Doe‘;
var obj = {
fullname: ‘Colin Ihrig‘,
prop: {
fullname: ‘Aurelio De Rosa‘,
getFullname: function() {
return this.fullname;
}
}
};
console.log(obj.prop.getFullname());
var test = obj.prop.getFullname;
console.log(test());
這段代碼打印結果是:Aurelio De Rosa
和 John Doe
。原因是,JavaScript中關鍵字this
所引用的是函數上下文,取決於函數是如何調用的,而不是怎麽被定義的。
在第一個console.log()
,getFullname()
是作為obj.prop
對象的函數被調用。因此,當前的上下文指代後者,並且函數返回這個對象的fullname
屬性。相反,當getFullname()
被賦值給test
變量時,當前的上下文是全局對象window
,這是因為test
被隱式地作為全局對象的屬性。基於這一點,函數返回window
的fullname
,在本例中即為第一行代碼設置的。
問題5: call() 和 apply()
修復前一個問題,讓最後一個console.log()
打印輸出Aurelio De Rosa。
這個問題可以通過運用call()
或者apply()
方法強制轉換上下文環境。下面的代碼中,我用了call(),
但apply()
也能產生同樣的結果:
console.log(test.call(obj.prop));
問題6: 閉包(Closures)
考慮下面的代碼:
var nodes = document.getElementsByTagName(‘button‘);
for (var i = 0; i < nodes.length; i++) {
nodes[i].addEventListener(‘click‘, function() {
console.log(‘You clicked element #‘ + i);
});
}
請問,如果用戶點擊第一個和第四個按鈕的時候,控制臺分別打印的結果是什麽?為什麽?
代碼打印兩次You clicked element #NODES_LENGTH
,其中NODES_LENGTH
是nodes的結點個數。
原因是在for循環完成後,變量i
的值等於節點列表的長度。此外,因為i
在代碼添加處理程序的作用域中,該變量屬於處理程序的閉包。你會記得,閉包中的變量的值不是靜態的,因此i
的值不是添加處理程序時的值(對於列表來說,第一個按鈕為0,對於第二個按鈕為1,依此類推)。在處理程序將被執行的時候,在控制臺上將打印變量i
的當前值,等於節點列表的長度。
問題7: 閉包(Closures)
修復上題的問題,使得點擊第一個按鈕時輸出0,點擊第二個按鈕時輸出1,依此類推。
有多種辦法可以解決這個問題,下面主要使用兩種方法解決這個問題。
第一個解決方案使用立即執行函數表達式(IIFE)再創建一個閉包,從而得到所期望的i的值。實現此方法的代碼如下:
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,而是將函數移到循環的外面。這種方法由下面的代碼實現:
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));
}
問題8:數據類型
考慮如下代碼:
console.log(typeof null);//object
console.log(typeof {});//object
console.log(typeof []);//object
console.log(typeof undefined);//undefined
大多數開發人員認為typeof []
會返回Array
。如果你想測試一個變量是否為數組
var myArray = [];
if (myArray instanceof Array) {
// do something...
}
問題9:事件循環
下面代碼運行結果是什麽?請解釋。
function printing() {
console.log(1);
setTimeout(function() { console.log(2); }, 1000);
setTimeout(function() { console.log(3); }, 0);
console.log(4);
}
printing();
輸出結果:1 4 3 2
想知道為什麽輸出順序是這樣的,你需要弄了解setTimeout()
做了什麽,以及瀏覽器的事件循環原理。瀏覽器有一個事件循環用於檢查事件隊列,處理延遲的事件。UI事件(例如,點擊,滾動等),Ajax回調,以及提供給setTimeout()
和setInterval()
的回調都會依次被事件循環處理。因此,當調用setTimeout()
函數時,即使延遲的時間被設置為0
,提供的回調也會被排隊。回調會呆在隊列中,直到指定的時間用完後,引擎開始執行動作(如果它在當前不執行其他的動作)。因此,即使setTimeout()
回調被延遲0
毫秒,它仍然會被排隊,並且直到函數中其他非延遲的語句被執行完了之後,才會執行。
有了這些認識,理解輸出結果為“1”就容易了,因為它是函數的第一句並且沒有使用setTimeout()
函數來延遲。接著輸出“4”,因為它是沒有被延遲的數字,也沒有進行排隊。然後,剩下了“2”,“3”,兩者都被排隊,但是前者需要等待一秒,後者等待0秒(這意味著引擎完成前兩個輸出之後馬上進行)。這就解釋了為什麽“3”在“2”之前。
問題10:算法
寫一個isPrime()
函數,當其為質數時返回true
,否則返回false
。
我認為這是面試中最常見的問題之一。然而,盡管這個問題經常出現並且也很簡單,但是從被面試人提供的答案中能很好地看出被面試人的數學和算法水平。
首先, 因為JavaScript不同於C或者Java,因此你不能信任傳遞來的數據類型。如果面試官沒有明確地告訴你,你應該詢問他是否需要做輸入檢查,還是不進行檢查直接寫函數。嚴格上說,應該對函數的輸入進行檢查。
第二點要記住:負數不是質數。同樣的,1和0也不是,因此,首先測試這些數字。此外,2是質數中唯一的偶數。沒有必要用一個循環來驗證4,6,8。再則,如果一個數字不能被2整除,那麽它不能被4,6,8等整除。因此,你的循環必須跳過這些數字。如果你測試輸入偶數,你的算法將慢2倍(你測試雙倍數字)。可以采取其他一些更明智的優化手段,我這裏采用的是適用於大多數情況的。例如,如果一個數字不能被5整除,它也不會被5的倍數整除。所以,沒有必要檢測10,15,20等等。
最後一點,你不需要檢查比輸入數字的開方還要大的數字。我感覺人們會遺漏掉這一點,並且也不會因為此而獲得消極的反饋。但是,展示出這一方面的知識會給你額外加分。
現在你具備了這個問題的背景知識,下面是總結以上所有考慮的解決方案:
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;
}
10道典型的JavaScript面試題