幾道javascript練習題

走在前端的大道上javascript

問題1: 做用域(Scope)

考慮如下代碼:css

(function() {
   var a = b = 5;
})();
 
console.log(b);

控制檯(console)會打印出什麼?html

答案前端

上述代碼會打印出5。java

這個問題的陷阱就是,在當即執行函數表達式(IIFE)中,有兩個賦值,可是其中變量a使用關鍵詞var來聲明。這就意味着a是這個函數的局部變量。與此相反,b被分配給了全局做用域(譯註:也就是全局變量)。node

這個問題另外一個陷阱就是,在函數中沒有使用」嚴格模式」 ('use strict';)。若是 嚴格模式開啓,那麼代碼就會報錯 」 Uncaught ReferenceError: b is not defined」 。請記住,若是這是預期的行爲,嚴格模式要求你顯式地引用全局做用域。因此,你須要像下面這麼寫:web

(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方法時,這個技術很是有用。

另外一個:重複輸出一個給定的字符串(str第一個參數)n 次 (num第二個參數),若是第二個參數num不是正數的時候,返回空字符串。

function repeatStringNumTimes(str, num) {
  return str;
}
repeatStringNumTimes("abc", 3);

提供測試狀況:

repeatStringNumTimes("*", 3) //應該返回 "***".
repeatStringNumTimes("abc", 3) //應該返回 "abcabcabc".
repeatStringNumTimes("abc", 4) //應該返回 "abcabcabcabc".
repeatStringNumTimes("abc", 1) //應該返回 "abc".
repeatStringNumTimes("*", 8) //應該返回 "********".
repeatStringNumTimes("abc", -2) //應該返回 "".

解題思路:

三種方法:

使用 while 循環
使用遞歸
使用ES6 repeat()

方法1:經過 while 循環重複輸出一個字符串

這多是最常規的解題思路。while 語句只要指定的條件計算結果爲true的時候,就執行其語句。while 語句結構大概是這樣的:

while (condition)
  statement

在每次經過循環以前計算條件結果。若是條件爲true,則執行語句。若是條件爲false,則執行繼續 while 循環以後的任何語句。

只要條件爲true,語句就會執行。 這裏是解決方案:

function repeatStringNumTimes(string, times) {
  // 第1步. 常見一個空字符,用來寄存重複的字符串
  var repeatedString = "";
 
  // 第2步. 設置 while 循環的條件爲(times > 0) 做爲檢查
  while (times > 0) { // 只要 times 大於 0, 語句就會執行
    // 執行語句 statement
    repeatedString += string; // 等價於 repeatedString = repeatedString + string; 
    times--; // 遞減,等價於 times = times - 1; 
  }
  /* while循環邏輯
          條件        T/F    repeatedString += string   結果          次數
    1th   (3 > 0)    true    "" + "abc"                "abc"          2
    2th   (2 > 0)    true    "abc" + "abc"             "abcabc"       1
    3th   (1 > 0)    true    "abcabc" + "abc"          "abcabcabc"    0
    4th   (0 > 0)    false
    }
  */
  
  // 第3步. 返回重複字符串
  return repeatedString; // "abcabcabc"
}
 
repeatStringNumTimes("abc", 3);

去掉註釋後:

function repeatStringNumTimes(string, times) {
  var repeatedString = "";
  while (times > 0) {
    repeatedString += string;
    times--;
  }
  return repeatedString;
}
repeatStringNumTimes("abc", 3);

好,輕鬆完成!不過這裏還能夠有幾個變種:

對於老前端來講,首先一個可能會將字符串拼接,修改成 數組join()拼接字符串,例如:

function repeatStringNumTimes(string, times) {
  var repeatedArr = []; //
  while (times > 0) {
    repeatedArr.push(string);
    times--;
  }
  return repeatedArr.join("");
}
repeatStringNumTimes("abc", 3)

不少老前端都有用數組join()拼接字符串的「情懷」,由於很早之前廣泛認爲數組join()拼接字符串比字符串+拼接速度要快得多。不過如今未必,例如,V8 下+拼接字符串,要比數組join()拼接字符串快。我用這兩個方法測試了3萬次重複輸出,只相差了幾毫秒。

另外一個變種能夠用 for 循環:

function repeatStringNumTimes(string, times) {
  var repeatedString = "";
  for(var i = 0; i < times ;i++) {
    repeatedString += string;
  }
  return repeatedString;
}
repeatStringNumTimes("abc", 3)

方法2:經過條件判斷和遞歸重複輸出一個字符串

遞歸是一種經過重複地調用函數自己,直到它達到達結果爲止的迭代操做的技術。爲了使其正常工做,必須包括遞歸的一些關鍵特徵。

第一種是基本狀況:一個語句,一般在一個條件語句(如if)中,中止遞歸。

第二種是遞歸狀況:調用遞歸函數自己的語句。

這裏是解決方案:

function repeatStringNumTimes(string, times) {
  // 步驟1.檢查 times 是否爲負數,若是爲 true 則返回一個空字符串 
  if (times < 0) {
    return "";
  }
  
  // 步驟2.檢查times是否等於1,若是是,返回字符串自己。
  if (times === 1) {
    return string;
  }
  
  // 步驟3. 使用遞歸
  else {
    return string + repeatStringNumTimes(string, times - 1); // return "abcabcabc";
  }
  /* 
    遞歸方法的第一部分你須要記住,你不會只調用一次,您將有好幾個嵌套調用
                 times       string + repeatStringNumTimes(string, times - 1)
      1st call   3           "abc" + ("abc", 3 - 1)
      2nd call   2           "abc" + ("abc", 2 - 1)
      3rd call   1           "abc" => if (times === 1) return string;
      4th call   0           ""   => if (times <= 0) return "";
    遞歸方法的第二部分
      4th call will return      ""
      3rd call will return     "abc"
      2nd call will return     "abc"
      1st call will return     "abc"
    最後調用是串聯全部字符串
    return "abc" + "abc" + "abc"; // return "abcabcabc";
  */
}
repeatStringNumTimes("abc", 3);

去掉註釋後:

function repeatStringNumTimes(string, times) {
  if(times < 0) 
    return "";
  if(times === 1) 
    return string;
  else 
    return string + repeatStringNumTimes(string, times - 1);
}
repeatStringNumTimes("abc", 3);

方法3:使用ES6 repeat() 方法重複輸出一個字符串

這個解決方案比較新潮,您將使用 String.prototype.repeat() 方法:

repeat() 方法構造並返回一個新字符串,該字符串包含被鏈接在一塊兒的指定數量的字符串的副本。 這個方法有一個參數 count 表示重複次數,介於0和正無窮大之間的整數 : [0, +∞) 。表示在新構造的字符串中重複了多少遍原字符串。重複次數不能爲負數。重複次數必須小於 infinity,且長度不會大於最長的字符串。

這裏是解決方案:

function repeatStringNumTimes(string, times) {
  //步驟1.若是 times 爲正數,返回重複的字符串
  if (times > 0) { // (3 > 0) => true
    return string.repeat(times); // return "abc".repeat(3); => return "abcabcabc";
  }
  
  //Step 2. Else 若是times是負數,若是爲true則返回一個空字符串
  else {
    return "";
  }
}
 
repeatStringNumTimes("abc", 3);

去掉註釋後:

function repeatStringNumTimes(string, times) {
  if (times > 0)
    return string.repeat(times);
  else
    return "";
}
repeatStringNumTimes("abc", 3);

您可使用三元表達式做爲 if/else 語句的快捷方式,以下所示:

function repeatStringNumTimes(string, times) {
  return times > 0 ? string.repeat(times) : "";
}
repeatStringNumTimes("abc", 3);

問題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()方法強制轉換上下文環境。若是你不瞭解這兩個方法及它們的區別,我建議你看看這篇文章 function.call和function.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);
   });
}

請問,若是用戶點擊第一個和第四個按鈕的時候,控制檯分別打印的結果是什麼?爲何?

答案

上面的代碼考察了一個很是重要的 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的值。實現此方法的代碼以下:

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);
console.log(typeof {});
console.log(typeof []);
console.log(typeof undefined);

答案

前面的問題彷佛有點傻,但它考察 typeof 操做符的知識。不少JavaScript開發人員不知道typeof的一些特性。在此示例中,控制檯將顯示如下內容:

object
object
object
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等等。若是你深刻了解這個問題的解決方案,我建議你去看相關的Wikipedia介紹。

最後一點,你不須要檢查比輸入數字的開方還要大的數字。我感受人們會遺漏掉這一點,而且也不會由於此而得到消極的反饋。可是,展現出這一方面的知識會給你額外加分。

如今你具有了這個問題的背景知識,下面是總結以上全部考慮的解決方案:

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

問題11:數據類型

var a = {n : 1};
var b = a;
a.x = a = {n : 2};
console.log(a.x);  
console.log(b.x);

解析:

var a = {n : 1};
var b = a;
// 此時b = {n:1};
//若是此時a.n=4,那麼b.n也等於4
a.x = a = {n : 2};
// 從右往左賦值,a = {n:2}; 新對象
// b = {n:2},//此時筆者認爲b應該仍是{n:1}待考證確認
// a.x 中的a是{n:1}; {n:1}.x = {n:2}; 舊對象
// 由於b和a是引用的關係因此b.x也等於 {n:2}
console.log(a.x); undefined
// 此時的a是新對象,新對象上沒有a.x 因此是undefined
console.log(b.x); {n:2}
var i = 10;
i += i *= i;

// i*=i 100
// i+= 這裏的i是 =10不是100
console.log(i);

問題12:

if (!("a" in window)) {
    var a = 1;
}

console.log(a);

解析:

在瀏覽器環境中,全局變量都是window的一個屬性,即
var a = 1 等價於 window.a = 1。in操做符用來判斷某個屬性屬於某個對象,能夠是對象的直接屬性,也能夠是經過prototype繼承的屬性。

再看題目,在瀏覽器中,若是沒有全局變量 a ,則聲明一個全局變量 a (ES5沒有塊級做用域),而且賦值爲1。不少人會認爲打印的是1。非也,你們不要忘了變量聲明會被前置!什麼意思呢?題目也就等價於

var a;

if (!("a" in window)) {
    a = 1;
}

console.log(a);

因此其實已經聲明瞭變量a,只不過if語句以前值是undefined,因此if語句壓根不會執行。
最後答案就是 undefined

問題13:

var a = 1,
    b = function a(x) {
        x && a(--x);
    };
console.log(a);

解析:
這道題有幾個須要注意的地方:

1.變量聲明、函數聲明會被前置,可是函數表達式並不會,準確說相似變量聲明前置,舉個栗子:

console.log('b', b); // b undefined
var b = function() {}
console.log('b', b); // b function () {}

2.具名的函數表達式的名字只能在該函數內部取到,舉個例子(排除老的IE?):

var foo = function bar () {}

console.log('foo', foo); 
// foo function bar(){}

console.log('bar', bar);
// Uncaught ReferenceError: bar is not defined

綜合這兩點,再看題目,最後輸出的內容就爲 1

問題14:

function a(x) {
    return x * 2;
}
var a;
console.log(a);

解析:
函數聲明會覆蓋變量聲明,但不會覆蓋變量賦值,舉個栗子簡單粗暴:

function foo(){
    return 1;
}
var foo;
console.log(typeof foo);    // "function"

函數聲明的優先級高於變量聲明的優先級,但若是該變量foo賦值了,那結果就徹底不同了:

function foo(){
    return 1;
}
var foo = 1;
console.log(typeof foo);    // "number"

變量foo賦值之後,變量賦值初始化就覆蓋了函數聲明。這個須要注意
再看題目

function a(x) {
    return x * 2;
}
var a;
console.log(a); // function a(x) {...}

問題15:

function b(x, y, a) {
    arguments[2] = 10;
    console.log(a);
}
b(1, 2, 3);

解析:
這題考察 arguments 對象的用法(詳看JavaScript中的arguments對象)
通常狀況,arguments與函數參數是動態綁定關係(爲何說是通常稍後會解釋),因此很好理解,最後輸出的是10

可是可是可是,咱們不要忘了一個特殊狀況–嚴格模式,在嚴格模式中 arguments 與至關於函數參數的一個拷貝,並無動態綁定關係,舉個栗子:

'use strict'
// 嚴格模式!!

function b(x, y, a) {
    arguments[2] = 10;
    console.log(a);
}
b(1, 2, 3); // 3

問題16:

function a() {
    console.log(this);
}
a.call(null);

解析:

function a() {
    console.log(this);
}
a.call(null);

關於 a.call(null); 根據ECMAScript262規範規定:
若是第一個參數傳入的對象調用者是null或者undefined的話,call方法將把全局對象(瀏覽器上是window對象)做爲this的值。因此,無論你何時傳入null或者 undefined,其this都是全局對象window。因此,在瀏覽器上答案是輸出 window 對象。

可是可是可是,咱們依舊不能忘記一個特殊狀況–嚴格模式,在嚴格模式中,null 就是 null,undefined 就是 undefined ,舉個栗子:

'use strict';
// 嚴格模式!!

function a() {
    console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined

參考文章:
1.10道典型的JavaScript面試題
2.對匿名函數的深刻理解(完全版) 見評論區
3.你真的知道JS嗎?

相關文章
相關標籤/搜索