2018年,最經典的26個JavaScript面試題和答案!

java隋七哥發表於2018-11-19

根據 Stack Overflow 的 2018 年度調查,JavaScript 連續六年成為最常用的程式語言。所以我們必須面對這樣的現實,JavaScript 已經成為全棧開發技能的基石,在全棧開發面試中都會不可避免地涉及到與 JavaScript 有關的問題。FullStack.Cafe 彙編了最常見的 JavaScript 面試問題和答案,希望能夠幫助讀者找到下一份夢想中的工作。

Q1:JavaScript 中的強制轉型(coercion)是指什麼?

難度:0

在 JavaScript 中,兩種不同的內建型別間的轉換被稱為強制轉型。強制轉型在 JavaScript 中有兩種形式:顯式和隱式。

這是一個顯式強制轉型的例子:

var a = “42”;

var b = Number( a );

a;                // “42”

b;                // 42 — 是個數字!

這是一個隱式強制轉型的例子:

var a = “42”;

var b = a * 1;    // “42” 隱式轉型成 42

a;                // “42”

b;                // 42 — 是個數字!

Q2:JavaScript 中的作用域(scope)是指什麼?

難度:⭐

在 JavaScript 中,每個函式都有自己的作用域。作用域基本上是變數以及如何通過名稱訪問這些變數的規則的集合。只有函式中的程式碼才能訪問函式作用域內的變數。

同一個作用域中的變數名必須是唯一的。一個作用域可以巢狀在另一個作用域內。如果一個作用域巢狀在另一個作用域內,最內部作用域內的程式碼可以訪問另一個作用域的變數。

Q3:解釋 JavaScript 中的相等性。

難度:⭐

JavaScript 中有嚴格比較和型別轉換比較:

嚴格比較(例如 ===)在不允許強制轉型的情況下檢查兩個值是否相等;

抽象比較(例如 ==)在允許強制轉型的情況下檢查兩個值是否相等。

var a = “42”;

var b = 42;

a == b;            // true

a === b;        // false

一些簡單的規則:

如果被比較的任何一個值可能是 true 或 false,要用 ===,而不是 ==;

如果被比較的任何一個值是這些特定值(0、“”或 []),要用 ===,而不是 ==;

在其他情況下,可以安全地使用 ==。它不僅安全,而且在很多情況下,它可以簡化程式碼,並且提升程式碼可讀性。

Q4:解釋什麼是回撥函式,並提供一個簡單的例子。

難度:⭐⭐

回撥函式是可以作為引數傳遞給另一個函式的函式,並在某些操作完成後執行。下面是一個簡單的回撥函式示例,這個函式在某些操作完成後列印訊息到控制檯。

function modifyArray(arr, callback) {

  // 對 arr 做一些操作

  arr.push(100);

  // 執行傳進來的 callback 函式

  callback();

}

var arr = [1, 2, 3, 4, 5];

modifyArray(arr, function() {

  console.log(“array has been modified”, arr);

});

Q5:“use strict”的作用是什麼?

難度:⭐⭐

use strict 出現在 JavaScript 程式碼的頂部或函式的頂部,可以幫助你寫出更安全的 JavaScript 程式碼。如果你錯誤地建立了全域性變數,它會通過丟擲錯誤的方式來警告你。例如,以下程式將丟擲錯誤:

function doSomething(val) {

  “use strict”;

  x = val + 10;

}

它會丟擲一個錯誤,因為 x 沒有被定義,並使用了全域性作用域中的某個值對其進行賦值,而 use strict 不允許這樣做。下面的小改動修復了這個錯誤:

function doSomething(val) {

  “use strict”;

  var x = val + 10;

}

Q6:解釋 JavaScript 中的 null 和 undefined。

難度:⭐⭐

JavaScript 中有兩種底層型別:null 和 undefined。它們代表了不同的含義:

尚未初始化的東西:undefined;

目前不可用的東西:null。

Q7:編寫一個可以執行如下操作的函式。

難度:⭐⭐

var addSix = createBase(6);

addSix(10); // 返回 16

addSix(21); // 返回 27

可以建立一個閉包來存放傳遞給函式 createBase 的值。被返回的內部函式是在外部函式中建立的,內部函式就成了一個閉包,它可以訪問外部函式中的變數,在本例中是變數 baseNumber。

function createBase(baseNumber) {

  return function(N) {

    // 我們在這裡訪問 baseNumber,即使它是在這個函式之外宣告的。

    // JavaScript 中的閉包允許我們這麼做。

    return baseNumber + N;

  }

}

var addSix = createBase(6);

addSix(10);

addSix(21);

Q8:解釋 JavaScript 中的值和型別。

難度:⭐⭐

JavaScript 有型別值,但沒有型別變數。JavaScript 提供了以下幾種內建型別:

string

number

boolean

null 和 undefined

object

symbol (ES6 中新增的)

Q9:解釋事件冒泡以及如何阻止它?

難度:⭐⭐

事件冒泡是指巢狀最深的元素觸發一個事件,然後這個事件順著巢狀順序在父元素上觸發。

防止事件冒泡的一種方法是使用 event.cancelBubble 或 event.stopPropagation()(低於 IE 9)。

Q10:JavaScript 中的 let 關鍵字有什麼用?

難度:⭐⭐

除了可以在函式級別宣告變數之外,ES6 還允許你使用 let 關鍵字在程式碼塊({..})中宣告變數。

Q11:如何檢查一個數字是否為整數?

難度:⭐⭐

檢查一個數字是小數還是整數,可以使用一種非常簡單的方法,就是將它對 1 進行取模,看看是否有餘數。

function isInt(num) {

  return num % 1 === 0;

}

console.log(isInt(4)); // true

console.log(isInt(12.2)); // false

console.log(isInt(0.3)); // false

Q12:什麼是 IIFE(立即呼叫函式表示式)?

難度:⭐⭐⭐

它是立即呼叫函式表示式(Immediately-Invoked Function Expression),簡稱 IIFE。函式被建立後立即被執行:

(function IIFE(){

    console.log( “Hello!” );

})();

// “Hello!”

在避免汙染全域性名稱空間時經常使用這種模式,因為 IIFE(與任何其他正常函式一樣)內部的所有變數在其作用域之外都是不可見的。

Q13:如何在 JavaScript 中比較兩個物件?

難度:⭐⭐⭐

對於兩個非原始值,比如兩個物件(包括函式和陣列),== 和 === 比較都只是檢查它們的引用是否匹配,並不會檢查實際引用的內容。

例如,預設情況下,陣列將被強制轉型成字串,並使用逗號將陣列的所有元素連線起來。所以,兩個具有相同內容的陣列進行 == 比較時不會相等:

var a = [1,2,3];

var b = [1,2,3];

var c = “1,2,3”;

a == c;        // true

b == c;        // true

a == b;        // false

對於物件的深度比較,可以使用 deep-equal 這個庫,或者自己實現遞迴比較演算法。

推薦一個交流學習群,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

點選:加入

Q14:你能解釋一下 ES5 和 ES6 之間的區別嗎?

難度:⭐⭐⭐

ECMAScript 5(ES5):ECMAScript 的第 5 版,於 2009 年標準化。這個標準已在所有現代瀏覽器中完全實現。

ECMAScript 6(ES6)或 ECMAScript 2015(ES2015):第 6 版 ECMAScript,於 2015 年標準化。這個標準已在大多數現代瀏覽器中部分實現。

以下是 ES5 和 ES6 之間的一些主要區別:

箭頭函式和字串插值:

const greetings = (name) => {

    return `hello ${name}`;

}

const greetings = name => `hello ${name}`;

常量

常量在很多方面與其他語言中的常量一樣,但有一些需要注意的地方。常量表示對值的“固定引用”。因此,在使用常量時,你實際上可以改變變數所引用的物件的屬性,但無法改變引用本身。

const NAMES = [];

NAMES.push(“Jim”);

console.log(NAMES.length === 1); // true

NAMES = [“Steve”, “John”]; // error

塊作用域變數。

新的 ES6 關鍵字 let 允許開發人員宣告塊級別作用域的變數。let 不像 var 那樣可以進行提升。

預設引數值

預設引數允許我們使用預設值初始化函式。如果省略或未定義引數,則使用預設值,也就是說 null 是有效值。

// 基本語法

function multiply (a, b = 2) {

  return a * b;

}

multiply(5); // 10

類定義和繼承

ES6 引入了對類(關鍵字 class)、建構函式(關鍵字 constructor)和用於繼承的 extend 關鍵字的支援。

for…of 操作符

for…of 語句將建立一個遍歷可迭代物件的迴圈。

用於物件合併的 Spread 操作

const obj1 = { a: 1, b: 2 }

const obj2 = { a: 2, c: 3, d: 4}

const obj3 = {…obj1, …obj2}

promise

promise 提供了一種機制來處理非同步操作結果。你可以使用回撥來達到同樣的目的,但是 promise 通過方法連結和簡潔的錯誤處理帶來了更高的可讀性。

const isGreater = (a, b) => {

return new Promise ((resolve, reject) => {

  if(a > b) {

    resolve(true)

  } else {

    reject(false)

  }

  })

}

isGreater(1, 2)

.then(result => {

  console.log(`greater`)

})

.catch(result => {

  console.log(`smaller`)

})

模組匯出和匯入

const myModule = { x: 1, y: () => { console.log(`This is ES5`) }}

export default myModule;

import myModule from `./myModule`;

問題 15:解釋 JavaScript 中“undefined”和“not defined”之間的區別。

難度:⭐⭐⭐

在 JavaScript 中,如果你試圖使用一個不存在且尚未宣告的變數,JavaScript 將丟擲錯誤“var name is not defined”,讓後指令碼將停止執行。但如果你使用 typeof undeclared_variable,它將返回 undefined。

在進一步討論之前,先讓我們理解宣告和定義之間的區別。

“var x”表示一個宣告,因為你沒有定義它的值是什麼,你只是宣告它的存在。

var x; // 宣告 x

console.log(x); // 輸出: undefined

“var x = 1”既是宣告又是定義(我們也可以說它是初始化),x 變數的宣告和賦值相繼發生。在 JavaScript 中,每個變數宣告和函式宣告都被帶到了當前作用域的頂部,然後進行賦值,這個過程被稱為提升(hoisting)。

當我們試圖訪問一個被宣告但未被定義的變數時,會出現 undefined 錯誤。

var x; // 宣告

if(typeof x === `undefined`) // 將返回 true

當我們試圖引用一個既未宣告也未定義的變數時,將會出現 not defined 錯誤。

console.log(y); // 輸出: ReferenceError: y is not defined

Q16:匿名和命名函式有什麼區別?

難度:⭐⭐⭐

var foo = function() { // 賦給變數 foo 的匿名函式

    // ..

};

var x = function bar(){ // 賦給變數 x 的命名函式 bar

    // ..

};

foo(); // 實際執行函式

x();

Q17:Javascript 中的“閉包”是什麼?舉個例子?

難度:⭐⭐⭐⭐

閉包是在另一個函式(稱為父函式)中定義的函式,並且可以訪問在父函式作用域中宣告和定義的變數。

閉包可以訪問三個作用域中的變數:

在自己作用域中宣告的變數;

在父函式中宣告的變數;

在全域性作用域中宣告的變數。

var globalVar = “abc”;

// 自呼叫函式

(function outerFunction (outerArg) { // outerFunction 作用域開始

  // 在 outerFunction 函式作用域中宣告的變數

  var outerFuncVar = `x`;   

  // 閉包自呼叫函式

  (function innerFunction (innerArg) { // innerFunction 作用域開始

    // 在 innerFunction 函式作用域中宣告的變數

    var innerFuncVar = “y”;

    console.log(       

      “outerArg = ” + outerArg + ”
” +

      “outerFuncVar = ” + outerFuncVar + ”
” +

      “innerArg = ” + innerArg + ”
” +

      “innerFuncVar = ” + innerFuncVar + ”
” +

      “globalVar = ” + globalVar);

  // innerFunction 作用域結束

  })(5); // 將 5 作為引數

// outerFunction 作用域結束

})(7); // 將 7 作為引數

innerFunction 是在 outerFunction 中定義的閉包,可以訪問在 outerFunction 作用域內宣告和定義的所有變數。除此之外,閉包還可以訪問在全域性名稱空間中宣告的變數。

上述程式碼的輸出將是:

outerArg = 7

outerFuncVar = x

innerArg = 5

innerFuncVar = y

globalVar = abc

Q18:如何在 JavaScript 中建立私有變數?

難度:⭐⭐⭐⭐

要在 JavaScript 中建立無法被修改的私有變數,你需要將其建立為函式中的區域性變數。即使這個函式被呼叫,也無法在函式之外訪問這個變數。例如:

function func() {

  var priv = “secret code”;

}

console.log(priv); // throws error

要訪問這個變數,需要建立一個返回私有變數的輔助函式。

function func() {

  var priv = “secret code”;

  return function() {

    return priv;

  }

}

var getPriv = func();

console.log(getPriv()); // => secret code

Q19:請解釋原型設計模式。

難度:⭐⭐⭐⭐

原型模式可用於建立新物件,但它建立的不是非初始化的物件,而是使用原型物件(或樣本物件)的值進行初始化的物件。原型模式也稱為屬性模式。

原型模式在初始化業務物件時非常有用,業務物件的值與資料庫中的預設值相匹配。原型物件中的預設值被複制到新建立的業務物件中。

經典的程式語言很少使用原型模式,但作為原型語言的 JavaScript 在構造新物件及其原型時使用了這個模式。

Q20:判斷一個給定的字串是否是同構的。

難度:⭐⭐⭐⭐

如果兩個字串是同構的,那麼字串 A 中所有出現的字元都可以用另一個字元替換,以便獲得字串 B,而且必須保留字元的順序。字串 A 中的每個字元必須與字串 B 的每個字元一對一對應。

paper 和 title 將返回 true。

egg 和 sad 將返回 false。

dgg 和 add 將返回 true。

isIsomorphic(“egg”, `add`); // true

isIsomorphic(“paper”, `title`); // true

isIsomorphic(“kick”, `side`); // false

function isIsomorphic(firstString, secondString) {

  // 檢查長度是否相等,如果不相等, 它們不可能是同構的

  if (firstString.length !== secondString.length) return false

  var letterMap = {};

  for (var i = 0; i < firstString.length; i++) {

    var letterA = firstString[i],

        letterB = secondString[i];

    // 如果 letterA 不存在, 建立一個 map,並將 letterB 賦值給它

    if (letterMap[letterA] === undefined) {

      letterMap[letterA] = letterB;

    } else if (letterMap[letterA] !== letterB) {

      // 如果 letterA 在 map 中已存在, 但不是與 letterB 對應,

      // 那麼這意味著 letterA 與多個字元相對應。

      return false;

    }

  }

  // 迭代完畢,如果滿足條件,那麼返回 true。

  // 它們是同構的。

  return true;

}

Q21:“Transpiling”是什麼意思?

難度:⭐⭐⭐⭐

對於語言中新加入的語法,無法進行 polyfill。因此,更好的辦法是使用一種工具,可以將較新程式碼轉換為較舊的等效程式碼。這個過程通常稱為轉換(transpiling),就是 transforming + compiling 的意思。

通常,你會將轉換器(transpiler)加入到構建過程中,類似於 linter 或 minifier。現在有很多很棒的轉換器可選擇:

Babel:將 ES6+ 轉換為 ES5

Traceur:將 ES6、ES7 轉換為 ES5

Q22:“this”關鍵字的原理是什麼?請提供一些程式碼示例。

難度:⭐⭐⭐⭐

在 JavaScript 中,this 是指正在執行的函式的“所有者”,或者更確切地說,指將當前函式作為方法的物件。

function foo() {

    console.log( this.bar );

}

var bar = “global”;

var obj1 = {

    bar: “obj1”,

    foo: foo

};

var obj2 = {

    bar: “obj2”

};

foo();            // “global”

obj1.foo();        // “obj1”

foo.call( obj2 );  // “obj2”

new foo();        // undefined

Q23:如何向 Array 物件新增自定義方法,讓下面的程式碼可以執行?

難度:⭐⭐⭐⭐

var arr = [1, 2, 3, 4, 5];

var avg = arr.average();

console.log(avg);

JavaScript 不是基於類的,但它是基於原型的語言。這意味著每個物件都連結到另一個物件(也就是物件的原型),並繼承原型物件的方法。你可以跟蹤每個物件的原型鏈,直到到達沒有原型的 null 物件。我們需要通過修改 Array 原型來向全域性 Array 物件新增方法。

Array.prototype.average = function() {

  // 計算 sum 的值

  var sum = this.reduce(function(prev, cur) { return prev + cur; });

  // 將 sum 除以元素個數並返回

  return sum / this.length;

}

var arr = [1, 2, 3, 4, 5];

var avg = arr.average();

console.log(avg); // => 3

Q24:什麼是 JavaScript 中的提升操作?

難度:⭐⭐⭐⭐

提升(hoisting)是 JavaScript 直譯器將所有變數和函式宣告移動到當前作用域頂部的操作。有兩種型別的提升:

變數提升——非常少見

函式提升——更常見

無論 var(或函式宣告)出現在作用域的什麼地方,它都屬於整個作用域,並且可以在該作用域內的任何地方訪問它。

var a = 2;

foo();                  // 因為`foo()`宣告被”提升”,所以可呼叫

function foo() {

    a = 3;

    console.log( a );    // 3

    var a;              // 宣告被”提升”到 foo() 的頂部

}

console.log( a );    // 2

Q25:以下程式碼輸出的結果是什麼?

難度:⭐⭐⭐⭐

0.1 + 0.2 === 0.3

這段程式碼的輸出是 false,這是由浮點數內部表示導致的。0.1 + 0.2 並不剛好等於 0.3,實際結果是 0.30000000000000004。解決這個問題的一個辦法是在對小數進行算術運算時對結果進行舍入。

Q26:請描述一下 Revealing Module Pattern 設計模式。

難度:⭐⭐⭐⭐⭐

暴露模組模式(Revealing Module Pattern)是模組模式的一個變體,目的是維護封裝性並暴露在物件中返回的某些變數和方法。如下所示:

var Exposer = (function() {

  var privateVariable = 10;

  var privateMethod = function() {

    console.log(`Inside a private method!`);

    privateVariable++;

  }

  var methodToExpose = function() {

    console.log(`This is a method I want to expose!`);

  }

  var otherMethodIWantToExpose = function() {

    privateMethod();

  }

  return {

      first: methodToExpose,

      second: otherMethodIWantToExpose

  };

})();

Exposer.first();        // 輸出: This is a method I want to expose!

Exposer.second();      // 輸出: Inside a private method!

Exposer.methodToExpose; // undefined

它的一個明顯的缺點是無法引用私有方法。

推薦一個交流學習群,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

點選:加入


相關文章