在IT界中,JavaScript開發人員的需求量一直居高不下。如果你的能力能夠勝任這一角色,那麼你有很多機會換一家公司,并提高薪水。但在你被一家公司聘用之前,你必須展示你的技能,以通過面試環節。在本文中,我将向您展示10個 JavaScript 技能典型問題以及其相關解決方案,來面試前端工程師。它們很有意思!
問題1: 作用域(Scope)
考慮以下代碼:
JavaScript 代碼:
(function() {
var a = b = ;
})();
console.log(b);
控制台(console)會列印出什麼?
答案
上述代碼會列印出5。
這個問題的陷阱就是,在立即執行函數表達式(IIFE)中,有兩個指派,但是其中變量a使用關鍵詞var來聲明。這就意味着a是這個函數的局部變量。與此相反,b被配置設定給了全局作用域(譯注:也就是全局變量)。
這個問題另一個陷阱就是,在函數中沒有使用”嚴格模式” (‘use strict’;)。如果 嚴格模式開啟,那麼代碼就會報錯 ” Uncaught ReferenceError: b is not defined” 。請記住,如果這是預期的行為,嚴格模式要求你顯式地引用全局作用域。是以,你需要像下面這麼寫:
JavaScript 代碼:
(function() {
‘use strict’;
var a = window.b = 5;
})();
console.log(b);
問題2: 建立 “原生(native)” 方法
在 String 對象上定義一個 repeatify 函數。這個函數接受一個整數參數,來明确字元串需要重複幾次。這個函數要求字元串重複指定的次數。舉個例子:
JavaScript 代碼:
console.log('hello'.repeatify());
應該列印出hellohellohello.
答案
一個可行的做法如下:
JavaScript 代碼:
String.prototype.repeatify = String.prototype.repeatify || function(times) {
var str = '';
for (var i = ; i < times; i++) {
str += this;
}
return str;
};
這個問題測試了開發人員對 javascript 中繼承及原型(prototype)屬性的知識。這也驗證了開發人員是否有能力擴充原生資料類型功能(雖然不應該這麼做)。
在這裡,另一個關鍵點是,看你怎樣避免重寫可能已經定義了的方法。這可以通過在定義自己的方法之前,檢測方法是否已經存在。
JavaScript 代碼:
String.prototype.repeatify = String.prototype.repeatify || function(times) {};
當你被問起去擴充一個Javascript方法時,這個技術非常有用。
愚人碼頭譯注:重複輸出一個給定的字元串的解決方案可以看看這篇文章。也許面試官考你的是知識的廣度和對新知識的掌握情況。
問題3: 變量提升(Hoisting)
執行以下代碼的結果是什麼?為什麼?
JavaScript 代碼:
function test() {
console.log(a);
console.log(foo());
var a = ;
function foo() {
return ;
}
}
test();
答案
這段代碼的執行結果是undefined 和 。
這個結果的原因是,變量和函數都被提升(hoisted) 到了函數體的頂部。是以,當列印變量a時,它雖存在于函數體(因為a已經被聲明),但仍然是undefined。換言之,上面的代碼等同于下面的代碼:
JavaScript 代碼:
function test() {
var a;
function foo() {
return ;
}
console.log(a);
console.log(foo());
a = ;
}
test();
問題4: 在javascript中,
this
是如何工作的
以下代碼的結果是什麼?請解釋你的答案。
JavaScript 代碼:
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()方法強制轉換上下文環境。如果你不了解這兩個方法及它們的差別,我建議你看看這篇文章 function.call和function.apply之間有和差別?。 下面的代碼中,我用了call(),但apply()也能産生同樣的結果:
JavaScript 代碼:
console.log(test.call(obj.prop));
問題6: 閉包(Closures)
考慮下面的代碼:
JavaScript 代碼:
var nodes = document.getElementsByTagName('button');
for (var i = ; i < nodes.length; i++) {
nodes[i].addEventListener('click', function() {
console.log('You clicked element #' + i);
});
}
請問,如果使用者點選第一個和第四個按鈕的時候,控制台分别列印的結果是什麼?為什麼?
答案
上面的代碼考察了一個非常重要的 JavaScript 概念:閉包(Closures)。對于每一個JavaScript開發者來說,如果你想在網頁中編寫5行以上的代碼,那麼準确了解和恰當使用閉包是非常重要的。如果你想開始學習或者隻是想簡單地溫習一下閉包,那麼我強烈建議你去閱讀 Colin Ihrig 這個教程:JavaScript Closures Demystified
也就是說,代碼列印兩次You clicked element #NODES_LENGTH,其中NODES_LENGTH是nodes的結點個數。原因是在for循環完成後,變量i的值等于節點清單的長度。此外,因為i在代碼添加處理程式的作用域中,該變量屬于處理程式的閉包。你會記得,閉包中的變量的值不是靜态的,是以i的值不是添加處理程式時的值(對于清單來說,第一個按鈕為0,對于第二個按鈕為1,依此類推)。在處理程式将被執行的時候,在控制台上将列印變量i的目前值,等于節點清單的長度。
問題7: 閉包(Closures)
修複上題的問題,使得點選第一個按鈕時輸出0,點選第二個按鈕時輸出1,依此類推。
答案
有多種辦法可以解決這個問題,下面主要使用兩種方法解決這個問題。
第一個解決方案使用立即執行函數表達式(IIFE)再建立一個閉包,進而得到所期望的i的值。實作此方法的代碼如下:
JavaScript 代碼:
var nodes = document.getElementsByTagName('button');
for (var i = ; i < nodes.length; i++) {
nodes[i].addEventListener('click', (function(i) {
return function() {
console.log('You clicked element #' + i);
}
})(i));
}
另一個解決方案不使用IIFE,而是将函數移到循環的外面。這種方法由下面的代碼實作:
JavaScript 代碼:
function handlerWrapper(i) {
return function() {
console.log('You clicked element #' + i);
}
}
var nodes = document.getElementsByTagName('button');
for (var i = ; i < nodes.length; i++) {
nodes[i].addEventListener('click', handlerWrapper(i));
}
問題8:資料類型
考慮如下代碼:
JavaScript 代碼:
console.log(typeof null);
console.log(typeof {});
console.log(typeof []);
console.log(typeof undefined);
答案
前面的問題似乎有點傻,但它考察 typeof 操作符的知識。很多JavaScript開發人員不知道typeof的一些特性。在此示例中,控制台将顯示以下内容:
JavaScript 代碼:
object
object
object
undefined
最令人驚訝的輸出結果可能是第三個。大多數開發人員認為typeof []會傳回Array。如果你想測試一個變量是否為數組,您可以執行以下測試:
JavaScript 代碼:
var myArray = [];
if (myArray instanceof Array) {
// do something...
}
問題9:事件循環
下面代碼運作結果是什麼?請解釋。
JavaScript 代碼:
function printing() {
console.log();
setTimeout(function() { console.log(2); }, 1000);
setTimeout(function() { console.log(3); }, 0);
console.log();
}
printing();
答案
輸出結果:
JavaScript 代碼:
想知道為什麼輸出順序是這樣的,你需要弄了解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等等。如果你深入了解這個問題的解決方案,我建議你去看相關的Wikipedia介紹。
最後一點,你不需要檢查比輸入數字的開方還要大的數字。我感覺人們會遺漏掉這一點,并且也不會因為此而獲得消極的回報。但是,展示出這一方面的知識會給你額外加分。
現在你具備了這個問題的背景知識,下面是總結以上所有考慮的解決方案:
JavaScript 代碼:
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 < ) {
return false;
}
if (number === ) {
return true;
} else if (number % === ) {
return false;
}
var squareRoot = Math.sqrt(number);
for(var i = ; i <= squareRoot; i += ) {
if (number % i === ) {
return false;
}
}
return true;
}
結論
本文我們讨論了5個在對Javascript開發者面試中常問起的典型問題。實際中的問題會因面試的不同而不同,來自面試的真實問題可能會有所不同,但是涵蓋的概念和主題通常都是十分相似的。我希望你愉悅地測試你的能力。萬一你不知道所有的答案,不要擔心:沒有學習和經驗不能解決的問題。 如果你在面試中被問到了其他有趣的問題,不要猶豫馬上來和我們分享吧。這會幫助到很多的開發者。
在這篇文章中,在一些問題和練習的幫助下,我讨論了其他 JavaScript 重要概念,這些概念通常是前端開發人員角色面試的一部分。我希望你成功地回答所有這些問題,或者你學到了新的東西,以便你可以在你的下一次面試中表現更好。
原文:
https://www.sitepoint.com/-typical-javascript-interview-exercises/
https://www.sitepoint.com/-javascript-interview-exercises/
5道基礎面試題
題目1
if(!("a" in window)){
var a =;
}
alert(a);
代碼看起來是想說:如果window不包含屬性a,就聲明一個變量a,然後指派為1。
你可能認為alert出來的結果是1,然後實際結果是“undefined”。要了解為什麼,我們需要知道JavaScript裡的3個概念。
首先,所有的全局變量都是window的屬性,語句 var a = 1;等價于window.a = 1; 你可以用如下方式來檢測全局變量是否聲明:
第二,所有的變量聲明都在範圍作用域的頂部,看一下相似的例子:
alert("a" in window);
var a;
此時,盡管聲明是在alert之後,alert彈出的依然是true,這是因為JavaScript引擎首先會掃墓所有的變量聲明,然後将這些變量聲明移動到頂部,最終的代碼效果是這樣的:
var a;
alert("a" in window);
這樣看起來就很容易解釋為什麼alert結果是true了。
第三,你需要了解該題目的意思是,變量聲明被提前了,但變量指派沒有,因為這行代碼包括了變量聲明和變量指派。
你可以将語句拆分為如下代碼:
var a; //聲明
a = ; //初始化指派
當變量聲明和指派在一起用的時候,JavaScript引擎會自動将它分為兩部以便将變量聲明提前,不将指派的步驟提前是因為他有可能影響代碼執行出不可預期的結果。
是以,知道了這些概念以後,重新回頭看一下題目的代碼,其實就等價于:
var a;
if (!("a" in window)) {
a = ;
}
alert(a);
這樣,題目的意思就非常清楚了:首先聲明a,然後判斷a是否在存在,如果不存在就指派為1,很明顯a永遠在window裡存在,這個指派語句永遠不會執行,是以結果是undefined。
注:提前這個詞語顯得有點迷惑了,其實就是執行上下文的關系,因為執行上下文分2個階段:進入執行上下文和執行代碼,在進入執行上下文的時候,建立變量對象VO裡已經有了:函數的所有形參、所有的函數聲明、所有的變量聲明
VO(global) = {
a: undefined
}
這個時候a已經有了;
然後執行代碼的時候才開始走if語句。
注:相信很多人都是認為a在裡面不可通路,結果才是undefined的吧,其實是已經有了,隻不過初始值是undefined,而不是不可通路。
題目2
var a = ,
b = function a(x) {
x && a(--x);
};
alert(a);
這個題目看起來比實際複雜,alert的結果是1;這裡依然有3個重要的概念需要我們知道。
首先,在題目1裡我們知道了變量聲明在進入執行上下文就完成了;第二個概念就是函數聲明也是提前的,所有的函數聲明都在執行代碼之前都已經完成了聲明,和變
量聲明一樣。澄清一下,函數聲明是如下這樣的代碼:
function functionName(arg1, arg2){
//函數體
}
如下不是函數,而是函數表達式,相當于變量指派:
var functionName = function(arg1, arg2){
//函數體
};
澄清一下,函數表達式沒有提前,就相當于平時的變量指派。
第三需要知道的是,函數聲明會覆寫變量聲明,但不會覆寫變量指派,為了解釋這個,我們來看一個例子:
function value(){
return ;
}
var value;
alert(typeof value); //"function"
盡快變量聲明在下面定義,但是變量value依然是function,也就是說這種情況下,函數聲明的優先級高于變量聲明的優先級,但如果該變量value指派了,那結果就完全不一樣了:
function value(){
return ;
}
var value = ;
alert(typeof value); //"number"
該value指派以後,變量指派初始化就覆寫了函數聲明。
重新回到題目,這個函數其實是一個有名函數表達式,函數表達式不像函數聲明一樣可以覆寫變量聲明,但你可以注意到,變量b是包含了該函數表達式,而該函數表達式的名字是a;不同的浏覽器對a這個名詞處理有點不一樣,在IE裡,會将a認為函數聲明,是以它被變量初始化覆寫了,就是說如果調用a(–x)的話就會出錯,而其它浏覽器在允許在函數内部調用a(–x),因為這時候a在函數外面依然是數字。基本上,IE裡調用b(2)的時候會出錯,但其它浏覽器則傳回undefined。
了解上述内容之後,該題目換成一個更準确和更容易了解的代碼應該像這樣:
var a = ,
b = function(x) {
x && b(--x);
};
alert(a);
題目3
function a(x) {
return x * ;
}
var a;
alert(a);
這個題目就是題目2裡的加的注釋了,也就是函數聲明和變量聲明的關系和影響,遇到同名的函數聲明,VO不會重新定義,是以這時候全局的VO應該是如下這樣的:
VO(global) = {
a: 引用了函數聲明“a”
}
題目4
function b(x, y, a) {
arguments[] = ;
alert(a);
}
b(, , );
Arguments對象是活動對象的一個屬性,它包括如下屬性:
callee — 指向目前函數的引用
length — 真正傳遞的參數個數
properties-indexes (字元串類型的整數) 屬性的值就是函數的參數值(按參數清單從左到右排列)。 properties-indexes内部元素的個數等于arguments.length. properties-indexes 的值和實際傳遞進來的參數之間是共享的。
這個共享其實不是真正的共享一個記憶體位址,而是2個不同的記憶體位址,使用JavaScript引擎來保證2個值是随時一樣的,當然這也有一個前提,那就是這個索引值要小于你傳入的參數個數,也就是說如果你隻傳入2個參數,而還繼續使用arguments[2]指派的話,就會不一緻,例如:
function b(x, y, a) {
arguments[] = ;
alert(a);
}
b(, );
這時候因為沒傳遞第三個參數a,是以指派10以後,alert(a)的結果依然是undefined,而不是10,但如下代碼彈出的結果依然是10,因為和a沒有關系。
function b(x, y, a) {
arguments[] = ;
alert(arguments[]);
}
b(, );
題目5
function a() {
alert(this);
}
a.call(null);
這個題目可以說是最簡單的,也是最詭異的,因為如果沒學到它的定義的話,打死也不會知道結果的,關于這個題目,我們先來了解2個概念。
首先,就是this值是如何定義的,當一個方法在對象上調用的時候,this就指向到了該對象上,例如:
var object = {
method: function() {
alert(this === object); //true
}
}
object.method();
上面的代碼,調用method()的時候this被指向到調用它的object對象上,但在全局作用域裡,this是等價于window(浏覽器中,非浏覽器裡等價于global),在如果一個function的定義不是屬于一個對象屬性的時候(也就是單獨定義的函數),函數内部的this也是等價于window的,例如:
function method() {
alert(this === window); //true
}
method();
了解了上述概念之後,我們再來了解一下call()是做什麼的,call方法作為一個function執行代表該方法可以讓另外一個對象作為調用者來調用,call方法的第一個參數是對象調用者,随後的其它參數是要傳給調用method的參數(如果聲明了的話),例如:
function method() {
alert(this === window);
}
method(); //true
method.call(document); //false
第一個依然是true沒什麼好說的,第二個傳入的調用對象是document,自然不會等于window,是以彈出了false。
另外,根據ECMAScript262規範規定:如果第一個參數傳入的對象調用者是null或者undefined的話,call方法将把全局對象(也就是window)作為this的值。是以,不管你什麼時候傳入null,其this都是全局對象window,是以該題目可以了解成如下代碼:
function a() {
alert(this);
}
a.call(window);
是以彈出的結果是[object Window]就很容易了解了。