根據 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 b = a * 1; // "42" 隐式轉型成 42
Q2:JavaScript 中的作用域(scope)是指什麼?
難度:⭐
在 JavaScript 中,每個函數都有自己的作用域。作用域基本上是變量以及如何通過名稱通路這些變量的規則的集合。隻有函數中的代碼才能通路函數作用域内的變量。
同一個作用域中的變量名必須是唯一的。一個作用域可以嵌套在另一個作用域内。如果一個作用域嵌套在另一個作用域内,最内部作用域内的代碼可以通路另一個作用域的變量。
Q3:解釋 JavaScript 中的相等性。
JavaScript 中有嚴格比較和類型轉換比較:
嚴格比較(例如 ===)在不允許強制轉型的情況下檢查兩個值是否相等;
抽象比較(例如 ==)在允許強制轉型的情況下檢查兩個值是否相等。
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 不允許這樣做。下面的小改動修複了這個錯誤:
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;
}
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 + "\n" +
"outerFuncVar = " + outerFuncVar + "\n" +
"innerArg = " + innerArg + "\n" +
"innerFuncVar = " + innerFuncVar + "\n" +
"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
要通路這個變量,需要建立一個傳回私有變量的輔助函數。
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 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;
console.log(avg); // => 3
Q24:什麼是 JavaScript 中的提升操作?
提升(hoisting)是 JavaScript 解釋器将所有變量和函數聲明移動到目前作用域頂部的操作。有兩種類型的提升:
變量提升——非常少見
函數提升——更常見
無論 var(或函數聲明)出現在作用域的什麼地方,它都屬于整個作用域,并且可以在該作用域内的任何地方通路它。
var a = 2;
foo(); // 因為`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
它的一個明顯的缺點是無法引用私有方法。