2019年度最常見(jiàn)的JavaScript面試題和答案

2019年度已經(jīng)過(guò)去了,2020年面試高峰期又來(lái)了。經(jīng)過(guò)2019年的學(xué)習(xí)和面試經(jīng)歷,統(tǒng)計(jì)了下面一些最常見(jiàn)的面試題。

JavaScript 中的強(qiáng)制轉(zhuǎn)型(coercion)是指什么?

難度:簡(jiǎn)單

在 JavaScript 中,兩種不同的內(nèi)置類型間的轉(zhuǎn)換被稱為強(qiáng)制轉(zhuǎn)型。強(qiáng)制轉(zhuǎn)型在 JavaScript 中有兩種形式:顯式和隱式。

這是一個(gè)顯式強(qiáng)制轉(zhuǎn)型的例子:

var a = "42";
var b = Number( a );
a;                // "42" -- 字符串
b;                // 42 -- 是個(gè)數(shù)字!

這是一個(gè)隱式強(qiáng)制轉(zhuǎn)型的例子:

var a = "42";
var b = a * 1;    // "42" 隱式轉(zhuǎn)型成 42 
a;                // "42"
b;                // 42 -- 是個(gè)數(shù)字!

JavaScript 中的作用域(scope)是指什么?

難度:簡(jiǎn)單

在 JavaScript 中,每個(gè)函數(shù)都有自己的作用域。作用域基本上是變量以及如何通過(guò)名稱訪問(wèn)這些變量的規(guī)則的集合。只有函數(shù)中的代碼才能訪問(wèn)函數(shù)作用域內(nèi)的變量。

同一個(gè)作用域中的變量名必須是唯一的。一個(gè)作用域可以嵌套在另一個(gè)作用域內(nèi)。如果一個(gè)作用域嵌套在另一個(gè)作用域內(nèi),最內(nèi)部作用域內(nèi)的代碼可以訪問(wèn)另一個(gè)作用域的變量。

解釋 JavaScript 中的相等性。

難度:簡(jiǎn)單

JavaScript 中有嚴(yán)格比較和類型轉(zhuǎn)換比較:

  • 嚴(yán)格比較(例如 ===)在不允許強(qiáng)制轉(zhuǎn)型的情況下檢查兩個(gè)值是否相等;
  • 抽象比較(例如 ==)在允許強(qiáng)制轉(zhuǎn)型的情況下檢查兩個(gè)值是否相等。
var a = "42";
var b = 42;
a == b;            // true
a === b;        // false

一些簡(jiǎn)單的規(guī)則:

  • 如果被比較的任何一個(gè)值可能是 true 或 false,要用 ===,而不是 ==;
  • 如果被比較的任何一個(gè)值是這些特定值(0、“”或 []),要用 ===,而不是 ==;
  • 在其他情況下,可以安全地使用 ==。它不僅安全,而且在很多情況下,它可以簡(jiǎn)化代碼,并且提升代碼可讀性。

解釋什么是回調(diào)函數(shù),并提供一個(gè)簡(jiǎn)單的例子。

難度:簡(jiǎn)單

回調(diào)函數(shù)是可以作為參數(shù)傳遞給另一個(gè)函數(shù)的函數(shù),并在某些操作完成后執(zhí)行。下面是一個(gè)簡(jiǎn)單的回調(diào)函數(shù)示例,這個(gè)函數(shù)在某些操作完成后打印消息到控制臺(tái)。

function modifyArray(arr, callback) {
  // 對(duì) arr 做一些操作
  arr.push(100);
  // 執(zhí)行傳進(jìn)來(lái)的 callback 函數(shù)
  callback();
}

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

modifyArray(arr, function() {
  console.log("array has been modified", arr);
});

“use strict”的作用是什么?

難度:簡(jiǎn)單

use strict 出現(xiàn)在 JavaScript 代碼的頂部或函數(shù)的頂部,可以幫助你寫(xiě)出更安全的 JavaScript 代碼。如果你錯(cuò)誤地創(chuàng)建了全局變量,它會(huì)通過(guò)拋出錯(cuò)誤的方式來(lái)警告你。例如,以下程序?qū)伋鲥e(cuò)誤:

function doSomething(val) {
  "use strict"; 
  x = val + 10;
}

它會(huì)拋出一個(gè)錯(cuò)誤,因?yàn)?x 沒(méi)有被定義,并使用了全局作用域中的某個(gè)值對(duì)其進(jìn)行賦值,而 use strict 不允許這樣做。下面的小改動(dòng)修復(fù)了這個(gè)錯(cuò)誤:

function doSomething(val) {
  "use strict"; 
  var x = val + 10;
}

解釋 JavaScript 中的 null 和 undefined。

難度:簡(jiǎn)單

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

  • 尚未初始化的東西:undefined
  • 目前不可用的東西:null
  • typeof 也不一樣

編寫(xiě)一個(gè)可以執(zhí)行如下操作的函數(shù)。

難度:較簡(jiǎn)單

var addSix = createBase(6);
addSix(10); // 返回 16
addSix(21); // 返回 27

可以創(chuàng)建一個(gè)閉包來(lái)存放傳遞給函數(shù) createBase 的值。被返回的內(nèi)部函數(shù)是在外部函數(shù)中創(chuàng)建的,內(nèi)部函數(shù)就成了一個(gè)閉包,它可以訪問(wèn)外部函數(shù)中的變量,在本例中是變量 baseNumber。

function createBase(baseNumber) {
  return function(N) {
    // 我們?cè)谶@里訪問(wèn) baseNumber,即使它是在這個(gè)函數(shù)之外聲明的。
    // JavaScript 中的閉包允許我們這么做。
    return baseNumber + N;
  }
}

var addSix = createBase(6);
addSix(10);
addSix(21);

解釋 JavaScript 中的值和類型

難度:簡(jiǎn)單

JavaScript 有類型值,但沒(méi)有類型變量。JavaScript 提供了以下幾種內(nèi)置類型:

  • string
  • number
  • boolean
  • null 和 undefined
  • object
  • symbol (ES6 中新增的)
  • bigint

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

難度:簡(jiǎn)單

事件冒泡是指嵌套最深的元素觸發(fā)一個(gè)事件,然后這個(gè)事件順著嵌套順序在父元素上觸發(fā)。

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

JavaScript 中的 let 關(guān)鍵字有什么用?

難度:簡(jiǎn)單

除了可以在函數(shù)級(jí)別聲明變量之外,ES6 還允許你使用 let 關(guān)鍵字在代碼塊({..})中聲明變量。

如何檢查一個(gè)數(shù)字是否為整數(shù)?

難度:簡(jiǎn)單

檢查一個(gè)數(shù)字是小數(shù)還是整數(shù),可以使用一種非常簡(jiǎn)單的方法,就是將它對(duì) 1 進(jìn)行取模,看看是否有余數(shù)。

function isInt(num) {
  return num % 1 === 0;
}

console.log(isInt(4)); // true
console.log(isInt(12.2)); // false
console.log(isInt(0.3)); // false

什么是 IIFE(立即調(diào)用函數(shù)表達(dá)式)?

難度:簡(jiǎn)單

它是立即調(diào)用函數(shù)表達(dá)式(Immediately-Invoked Function Expression),簡(jiǎn)稱 IIFE。函數(shù)被創(chuàng)建后立即被執(zhí)行:

(function IIFE(){
    console.log( "Hello!" );
})();
// "Hello!"

如何在 JavaScript 中比較兩個(gè)對(duì)象?

難度:中等

對(duì)于兩個(gè)非原始值,比如兩個(gè)對(duì)象(包括函數(shù)和數(shù)組),== 和 === 比較都只是檢查它們的引用是否匹配,并不會(huì)檢查實(shí)際引用的內(nèi)容。

例如,默認(rèn)情況下,數(shù)組將被強(qiáng)制轉(zhuǎn)型成字符串,并使用逗號(hào)將數(shù)組的所有元素連接起來(lái)。所以,兩個(gè)具有相同內(nèi)容的數(shù)組進(jìn)行 == 比較時(shí)不會(huì)相等:

var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";

a == c;        // true
b == c;        // true
a == b;        // false

乞丐版深拷貝

var obj1 = {
  a: 1,
  b: 2,
  c: 3
}
var objString = JSON.stringify(obj1);
var obj2 = JSON.parse(objString);
obj2.a = 5;
console.log(obj1.a);  // 1
console.log(obj2.a); // 5

對(duì)于對(duì)象的深度比較,可以使用 deep-equal 這個(gè)庫(kù),或者自己實(shí)現(xiàn)遞歸比較算法。

解釋一下 ES5 和 ES6 之間的區(qū)別嗎?

難度:中等

  • ECMAScript 5(ES5):ECMAScript 的第 5 版,于 2009 年標(biāo)準(zhǔn)化。這個(gè)標(biāo)準(zhǔn)已在所有現(xiàn)代瀏覽器中完全實(shí)現(xiàn)。
  • ECMAScript 6(ES6)或 ECMAScript 2015(ES2015):第 6 版 ECMAScript,于 2015 年標(biāo)準(zhǔn)化。這個(gè)標(biāo)準(zhǔn)已在大多數(shù)現(xiàn)代瀏覽器中部分實(shí)現(xiàn)。

具體可以去看 阮一峰老師的博客

Javascript 中的“閉包”是什么?舉個(gè)例子?

難度:中等

閉包是在另一個(gè)函數(shù)(稱為父函數(shù))中定義的函數(shù),并且可以訪問(wèn)在父函數(shù)作用域中聲明和定義的變量。

閉包可以訪問(wèn)三個(gè)作用域中的變量:

  • 在自己作用域中聲明的變量;
  • 在父函數(shù)中聲明的變量;
  • 在全局作用域中聲明的變量。

舉例:實(shí)現(xiàn)防抖 || 節(jié)流函數(shù)

如何在 JavaScript 中創(chuàng)建私有變量?

難度:中等

要在 JavaScript 中創(chuàng)建無(wú)法被修改的私有變量,你需要將其創(chuàng)建為函數(shù)中的局部變量。即使這個(gè)函數(shù)被調(diào)用,也無(wú)法在函數(shù)之外訪問(wèn)這個(gè)變量。例如:

function func() {
  var priv = "secret code";
}

console.log(priv); // throws error

要訪問(wèn)這個(gè)變量,需要?jiǎng)?chuàng)建一個(gè)返回私有變量的輔助函數(shù)。

function func() {
  var priv = "secret code";
  return function() {
    return priv;
  }
}

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

請(qǐng)解釋原型設(shè)計(jì)模式。

難度:中等

原型模式可用于創(chuàng)建新對(duì)象,但它創(chuàng)建的不是非初始化的對(duì)象,而是使用原型對(duì)象(或樣本對(duì)象)的值進(jìn)行初始化的對(duì)象。原型模式也稱為屬性模式。

原型模式在初始化業(yè)務(wù)對(duì)象時(shí)非常有用,業(yè)務(wù)對(duì)象的值與數(shù)據(jù)庫(kù)中的默認(rèn)值相匹配。原型對(duì)象中的默認(rèn)值被復(fù)制到新創(chuàng)建的業(yè)務(wù)對(duì)象中。

經(jīng)典的編程語(yǔ)言很少使用原型模式,但作為原型語(yǔ)言的 JavaScript 在構(gòu)造新對(duì)象及其原型時(shí)使用了這個(gè)模式。

判斷一個(gè)給定的字符串是否是同構(gòu)的。

難度:中等

如果兩個(gè)字符串是同構(gòu)的,那么字符串 A 中所有出現(xiàn)的字符都可以用另一個(gè)字符替換,以便獲得字符串 B,而且必須保留字符的順序。字符串 A 中的每個(gè)字符必須與字符串 B 的每個(gè)字符一對(duì)一對(duì)應(yīng)。

  • 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) {

  // 檢查長(zhǎng)度是否相等,如果不相等, 它們不可能是同構(gòu)的
  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 不存在, 創(chuàng)建一個(gè) map,并將 letterB 賦值給它
    if (letterMap[letterA] === undefined) {
      letterMap[letterA] = letterB;
    } else if (letterMap[letterA] !== letterB) {
      // 如果 letterA 在 map 中已存在, 但不是與 letterB 對(duì)應(yīng),
      // 那么這意味著 letterA 與多個(gè)字符相對(duì)應(yīng)。
      return false;
    }
  }
  // 迭代完畢,如果滿足條件,那么返回 true。
  // 它們是同構(gòu)的。
  return true;
}

“Transpiling”是什么意思?

難度:中等

對(duì)于語(yǔ)言中新加入的語(yǔ)法,無(wú)法進(jìn)行 polyfill。因此,更好的辦法是使用一種工具,可以將較新代碼轉(zhuǎn)換為較舊的等效代碼。這個(gè)過(guò)程通常稱為轉(zhuǎn)換(transpiling),就是 transforming + compiling 的意思。

通常,你會(huì)將轉(zhuǎn)換器(transpiler)加入到構(gòu)建過(guò)程中,類似于 linter 或 minifier?,F(xiàn)在有很多很棒的轉(zhuǎn)換器可選擇:

  • Babel:將 ES6+ 轉(zhuǎn)換為 ES5
  • Traceur:將 ES6、ES7 轉(zhuǎn)換為 ES5

“this”關(guān)鍵字的原理是什么?請(qǐng)?zhí)峁┮恍┐a示例。

難度:中等

在 JavaScript 中,this 是指正在執(zhí)行的函數(shù)的“所有者”,或者更確切地說(shuō),指將當(dāng)前函數(shù)作為方法的對(duì)象。

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

如何向 Array 對(duì)象添加自定義方法,讓下面的代碼可以運(yùn)行?

難度:中等

var arr = [1, 2, 3, 4, 5];
var avg = arr.average();
console.log(avg);

JavaScript 不是基于類的,但它是基于原型的語(yǔ)言。這意味著每個(gè)對(duì)象都鏈接到另一個(gè)對(duì)象(也就是對(duì)象的原型),并繼承原型對(duì)象的方法。你可以跟蹤每個(gè)對(duì)象的原型鏈,直到到達(dá)沒(méi)有原型的 null 對(duì)象。我們需要通過(guò)修改 Array 原型來(lái)向全局 Array 對(duì)象添加方法。

Array.prototype.average = function() {
  // 計(jì)算 sum 的值
  var sum = this.reduce(function(prev, cur) { return prev + cur; });
  // 將 sum 除以元素個(gè)數(shù)并返回
  return sum / this.length;
}

var arr = [1, 2, 3, 4, 5];
var avg = arr.average();
console.log(avg); // => 3

以下代碼輸出的結(jié)果是什么?

難度:中等

0.1 + 0.2 === 0.3

這段代碼的輸出是 false,這是由浮點(diǎn)數(shù)內(nèi)部表示導(dǎo)致的。0.1 + 0.2 并不剛好等于 0.3,實(shí)際結(jié)果是 0.30000000000000004。解決這個(gè)問(wèn)題的一個(gè)辦法是在對(duì)小數(shù)進(jìn)行算術(shù)運(yùn)算時(shí)對(duì)結(jié)果進(jìn)行舍入。

寫(xiě) React/Vue 項(xiàng)目時(shí)為什么要在組件中寫(xiě) key,其作用是什么?

難度:中等

key 的作用是為了在 diff 算法執(zhí)行時(shí)更快的找到對(duì)應(yīng)的節(jié)點(diǎn),提高 diff 速度。

vue 和 react 都是采用 diff 算法來(lái)對(duì)比新舊虛擬節(jié)點(diǎn),從而更新節(jié)點(diǎn)。在 vue 的 diff 函數(shù)中??梢韵攘私庖幌?diff 算法。

在交叉對(duì)比的時(shí)候,當(dāng)新節(jié)點(diǎn)跟舊節(jié)點(diǎn)頭尾交叉對(duì)比沒(méi)有結(jié)果的時(shí)候,會(huì)根據(jù)新節(jié)點(diǎn)的 key 去對(duì)比舊節(jié)點(diǎn)數(shù)組中的 key,從而找到相應(yīng)舊節(jié)點(diǎn)(這里對(duì)應(yīng)的是一個(gè) key => index 的 map 映射)。如果沒(méi)找到就認(rèn)為是一個(gè)新增節(jié)點(diǎn)。而如果沒(méi)有 key,那么就會(huì)采用一種遍歷查找的方式去找到對(duì)應(yīng)的舊節(jié)點(diǎn)。一種一個(gè) map 映射,另一種是遍歷查找。相比而言。map 映射的速度更快。

vue 部分源碼如下:

// vue 項(xiàng)目  src/core/vdom/patch.js  -488 行
// oldCh 是一個(gè)舊虛擬節(jié)點(diǎn)數(shù)組,
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
       idxInOld = isDef(newStartVnode.key)
         ? oldKeyToIdx[newStartVnode.key]
         : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

創(chuàng)建 map 函數(shù):

function createKeyToOldIdx (children, beginIdx, endIdx) {
 let i, key
 const map = {}
 for (i = beginIdx; i <= endIdx; ++i) {
   key = children[i].key
   if (isDef(key)) map[key] = i
 }
 return map
}

遍歷尋找:

// sameVnode 是對(duì)比新舊節(jié)點(diǎn)是否相同的函數(shù)
function findIdxInOld (node, oldCh, start, end) {
   for (let i = start; i < end; i++) {
     const c = oldCh[i]

     if (isDef(c) && sameVnode(node, c)) return i
   }
}

解析 ['1', '2', '3'].map(parseInt)

難度:中等

第一眼看到這個(gè)題目的時(shí)候,腦海跳出的答案是 [1, 2, 3],但是 真正的答案是 [1, NaN, NaN]。

首先讓我們回顧一下,map 函數(shù)的第一個(gè)參數(shù) callback:

  var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])

這個(gè) callback 一共可以接收三個(gè)參數(shù)

  • 其中第一個(gè)參數(shù)代表當(dāng)前被處理的元素,而第二個(gè)參數(shù)代表該元素的索引。
  • 而 parseInt 則是用來(lái)解析字符串的,使字符串成為指定基數(shù)的整數(shù)。
  • parseInt(string, radix)接收兩個(gè)參數(shù),第一個(gè)表示被處理的值(字符串),第二個(gè)表示為解析時(shí)的基數(shù)。

了解這兩個(gè)函數(shù)后,我們可以模擬一下運(yùn)行情況;

  • parseInt('1', 0) //radix 為 0 時(shí),且 string 參數(shù)不以“0x”和“0”開(kāi)頭時(shí),按照 10 為基數(shù)處理。這個(gè)時(shí)候返回 1;
  • parseInt('2', 1) // 基數(shù)為 1(1 進(jìn)制)表示的數(shù)中,最大值小于 2,所以無(wú)法解析,返回 NaN;
  • parseInt('3', 2) // 基數(shù)為 2(2 進(jìn)制)表示的數(shù)中,最大值小于 3,所以無(wú)法解析,返回 NaN。

map 函數(shù)返回的是一個(gè)數(shù)組,所以最后結(jié)果為 [1, NaN, NaN]。

什么是防抖和節(jié)流?有什么區(qū)別?如何實(shí)現(xiàn)?

難度:中等

防抖

觸發(fā)高頻事件后 n 秒內(nèi)函數(shù)只會(huì)執(zhí)行一次,如果 n 秒內(nèi)高頻事件再次被觸發(fā),則重新計(jì)算時(shí)間;

思路:每次觸發(fā)事件時(shí)都取消之前的延時(shí)調(diào)用方法:

乞丐版:

function debounce(fn) {
  let timeout = null; // 創(chuàng)建一個(gè)標(biāo)記用來(lái)存放定時(shí)器的返回值
  return function () {
    clearTimeout(timeout); // 每當(dāng)用戶輸入的時(shí)候把前一個(gè) setTimeout clear 掉
    timeout = setTimeout(() => { 
      // 然后又創(chuàng)建一個(gè)新的 setTimeout
      // 這樣就能保證輸入字符后的 interval 間隔內(nèi)如果還有字符輸入的話,就不會(huì)執(zhí)行 fn 函數(shù)
      fn.apply(this, arguments);
    }, 500);
  };
}
function sayHi() {
  console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
節(jié)流

高頻事件觸發(fā),但在 n 秒內(nèi)只會(huì)執(zhí)行一次,所以節(jié)流會(huì)稀釋函數(shù)的執(zhí)行頻率。

思路:每次觸發(fā)事件時(shí)都判斷當(dāng)前是否有等待執(zhí)行的延時(shí)函數(shù)。

乞丐版:

function throttle(fn) {
  let canRun = true; // 通過(guò)閉包保存一個(gè)標(biāo)記
  return function () {
    if (!canRun) return; // 在函數(shù)開(kāi)頭判斷標(biāo)記是否為 true,不為 true 則 return
    canRun = false; // 立即設(shè)置為 false
    setTimeout(() => { // 將外部傳入的函數(shù)的執(zhí)行放在 setTimeout 中
      fn.apply(this, arguments);
      // 最后在 setTimeout 執(zhí)行完畢后再把標(biāo)記設(shè)置為 true(關(guān)鍵) 表示可以執(zhí)行下一次循環(huán)了
      // 當(dāng)定時(shí)器沒(méi)有執(zhí)行的時(shí)候標(biāo)記永遠(yuǎn)是 false,在開(kāi)頭被 return 掉
      canRun = true;
    }, 500);
  };
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

介紹下 Set、Map、WeakSet 和 WeakMap 的區(qū)別?

難度:中等

Set

  • 成員唯一、無(wú)序且不重復(fù);
  • [value, value],鍵值與鍵名是一致的(或者說(shuō)只有鍵值,沒(méi)有鍵名);
  • 可以遍歷,方法有:add、delete、has。

WeakSet

  • 成員都是對(duì)象;
  • 成員都是弱引用,可以被垃圾回收機(jī)制回收,可以用來(lái)保存 DOM 節(jié)點(diǎn),不容易造成內(nèi)存泄漏;
  • 不能遍歷,方法有 add、delete、has。

Map

  • 本質(zhì)上是鍵值對(duì)的集合,類似集合;
  • 可以遍歷,方法很多,可以跟各種數(shù)據(jù)格式轉(zhuǎn)換。

WeakMap

  • 只接受對(duì)象最為鍵名(null 除外),不接受其他類型的值作為鍵名;
  • 鍵名是弱引用,鍵值可以是任意的,鍵名所指向的對(duì)象可以被垃圾回收,此時(shí)鍵名是無(wú)效的;
  • 不能遍歷,方法有 get、set、has、delete。

介紹下深度優(yōu)先遍歷和廣度優(yōu)先遍歷,如何實(shí)現(xiàn)?

難度:中等

深度優(yōu)先遍歷(DFS)

深度優(yōu)先遍歷(Depth-First-Search),是搜索算法的一種,它沿著樹(shù)的深度遍歷樹(shù)的節(jié)點(diǎn),盡可能深地搜索樹(shù)的分支。當(dāng)節(jié)點(diǎn) v 的所有邊都已被探尋過(guò),將回溯到發(fā)現(xiàn)節(jié)點(diǎn) v 的那條邊的起始節(jié)點(diǎn)。這一過(guò)程一直進(jìn)行到已探尋源節(jié)點(diǎn)到其他所有節(jié)點(diǎn)為止,如果還有未被發(fā)現(xiàn)的節(jié)點(diǎn),則選擇其中一個(gè)未被發(fā)現(xiàn)的節(jié)點(diǎn)為源節(jié)點(diǎn)并重復(fù)以上操作,直到所有節(jié)點(diǎn)都被探尋完成。

簡(jiǎn)單的說(shuō),DFS 就是從圖中的一個(gè)節(jié)點(diǎn)開(kāi)始追溯,直到最后一個(gè)節(jié)點(diǎn),然后回溯,繼續(xù)追溯下一條路徑,直到到達(dá)所有的節(jié)點(diǎn),如此往復(fù),直到?jīng)]有路徑為止。

DFS 可以產(chǎn)生相應(yīng)圖的拓?fù)渑判虮?,利用拓?fù)渑判虮砜梢越鉀Q很多問(wèn)題,例如最大路徑問(wèn)題。一般用堆數(shù)據(jù)結(jié)構(gòu)來(lái)輔助實(shí)現(xiàn) DFS 算法。

注意:深度 DFS 屬于盲目搜索,無(wú)法保證搜索到的路徑為最短路徑,也不是在搜索特定的路徑,而是通過(guò)搜索來(lái)查看圖中有哪些路徑可以選擇。

步驟:

  • 訪問(wèn)頂點(diǎn) v;
  • 依次從 v 的未被訪問(wèn)的鄰接點(diǎn)出發(fā),對(duì)圖進(jìn)行深度優(yōu)先遍歷;直至圖中和 v 有路徑相通的頂點(diǎn)都被訪問(wèn);
  • 若此時(shí)途中尚有頂點(diǎn)未被訪問(wèn),則從一個(gè)未被訪問(wèn)的頂點(diǎn)出發(fā),重新進(jìn)行深度優(yōu)先遍歷,直到所有頂點(diǎn)均被訪問(wèn)過(guò)為止。

實(shí)現(xiàn)

Graph.prototype.dfs = function() {
   var marked = []
   for (var i=0; i<this.vertices.length; i++) {
       if (!marked[this.vertices[i]]) {
           dfsVisit(this.vertices[i])
       }
   }

   function dfsVisit(u) {
       let edges = this.edges
       marked[u] = true
       console.log(u)
       var neighbors = edges.get(u)
       for (var i=0; i<neighbors.length; i++) {
           var w = neighbors[i]
           if (!marked[w]) {
               dfsVisit(w)
           }
       }
   }
}
廣度優(yōu)先遍歷(BFS)

廣度優(yōu)先遍歷(Breadth-First-Search)是從根節(jié)點(diǎn)開(kāi)始,沿著圖的寬度遍歷節(jié)點(diǎn),如果所有節(jié)點(diǎn)均被訪問(wèn)過(guò),則算法終止,BFS 同樣屬于盲目搜索,一般用隊(duì)列數(shù)據(jù)結(jié)構(gòu)來(lái)輔助實(shí)現(xiàn) BFS。

BFS 從一個(gè)節(jié)點(diǎn)開(kāi)始,嘗試訪問(wèn)盡可能靠近它的目標(biāo)節(jié)點(diǎn)。本質(zhì)上這種遍歷在圖上是逐層移動(dòng)的,首先檢查最靠近第一個(gè)節(jié)點(diǎn)的層,再逐漸向下移動(dòng)到離起始節(jié)點(diǎn)最遠(yuǎn)的層。

步驟:

  • 創(chuàng)建一個(gè)隊(duì)列,并將開(kāi)始節(jié)點(diǎn)放入隊(duì)列中;
  • 若隊(duì)列非空,則從隊(duì)列中取出第一個(gè)節(jié)點(diǎn),并檢測(cè)它是否為目標(biāo)節(jié)點(diǎn);
    • 若是目標(biāo)節(jié)點(diǎn),則結(jié)束搜尋,并返回結(jié)果;
    • 若不是,則將它所有沒(méi)有被檢測(cè)過(guò)的字節(jié)點(diǎn)都加入隊(duì)列中;
  • 若隊(duì)列為空,表示圖中并沒(méi)有目標(biāo)節(jié)點(diǎn),則結(jié)束遍歷。

實(shí)現(xiàn)

Graph.prototype.bfs = function(v) {
   var queue = [], marked = []
   marked[v] = true
   queue.push(v) // 添加到隊(duì)尾
   while(queue.length > 0) {
       var s = queue.shift() // 從隊(duì)首移除
       if (this.edges.has(s)) {
           console.log('visited vertex: ', s)
       }
       let neighbors = this.edges.get(s)
       for(let i=0;i<neighbors.length;i++) {
           var w = neighbors[i]
           if (!marked[w]) {
               marked[w] = true
               queue.push(w)
           }
       }
   }
}

請(qǐng)寫(xiě)出下面代碼的運(yùn)行結(jié)果:

難度:中等

async function async1() {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
}
async function async2() {
   console.log('async2')
}
console.log('script start')
setTimeout(function () {
   console.log('settimeout')
})
async1()
new Promise(function (resolve) {
   console.log('promise1')
   resolve()
}).then(function () {
   console.log('promise2')
})
console.log('script end')

題目的本質(zhì),就是考察setTimeout、promise、async await的實(shí)現(xiàn)及執(zhí)行順序,以及 JS 的事件循環(huán)的相關(guān)問(wèn)題。

答案

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout

將數(shù)組扁平化并去除其中重復(fù)數(shù)據(jù),最終得到一個(gè)升序且不重復(fù)的數(shù)組

難度:中等

Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})

JS 異步解決方案的發(fā)展歷程以及優(yōu)缺點(diǎn)

難度:中等

回調(diào)函數(shù)(callback)
setTimeout(() => {
   // callback 函數(shù)體
}, 1000)

缺點(diǎn):回調(diào)地獄,不能用 try catch 捕獲錯(cuò)誤,不能 return

回調(diào)地獄的根本問(wèn)題在于:

  • 缺乏順序性: 回調(diào)地獄導(dǎo)致的調(diào)試?yán)щy,和大腦的思維方式不符;
  • 嵌套函數(shù)存在耦合性,一旦有所改動(dòng),就會(huì)牽一發(fā)而動(dòng)全身,即(控制反轉(zhuǎn));
  • 嵌套函數(shù)過(guò)多的多話,很難處理錯(cuò)誤。
ajax('XXX1', () => {
   // callback 函數(shù)體
   ajax('XXX2', () => {
       // callback 函數(shù)體
       ajax('XXX3', () => {
           // callback 函數(shù)體
       })
   })
})
Promise

Promise 就是為了解決 callback 的問(wèn)題而產(chǎn)生的。

Promise 實(shí)現(xiàn)了鏈?zhǔn)秸{(diào)用,也就是說(shuō)每次 then 后返回的都是一個(gè)全新 Promise,如果我們?cè)?then 中 return ,return 的結(jié)果會(huì)被 Promise.resolve() 包裝。

優(yōu)點(diǎn):解決了回調(diào)地獄的問(wèn)題。

ajax('XXX1')
 .then(res => {
     // 操作邏輯
     return ajax('XXX2')
 }).then(res => {
     // 操作邏輯
     return ajax('XXX3')
 }).then(res => {
     // 操作邏輯
 })
Generator

特點(diǎn):可以控制函數(shù)的執(zhí)行,可以配合 co.js 函數(shù)庫(kù)使用。(也就是 koa早期使用的庫(kù))

function *fetch() {
   yield ajax('XXX1', () => {})
   yield ajax('XXX2', () => {})
   yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
Async/await

async、await 是異步的終極解決方案。

優(yōu)點(diǎn)是:代碼清晰,不用像 Promise 寫(xiě)一大堆 then 鏈,處理了回調(diào)地獄的問(wèn)題;

缺點(diǎn):await 將異步代碼改造成同步代碼,如果多個(gè)異步操作沒(méi)有依賴性而使用 await 會(huì)導(dǎo)致性能上的降低。

async function test() {
 // 以下代碼沒(méi)有依賴性的話,完全可以使用 Promise.all 的方式
 // 如果有依賴性的話,其實(shí)就是解決回調(diào)地獄的例子了
 await fetch('XXX1')
 await fetch('XXX2')
 await fetch('XXX3')
}

下面來(lái)看一個(gè)使用 await 的例子:

let a = 0
let b = async () => {
 a = a + await 10
 console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

上述解釋中提到了 await 內(nèi)部實(shí)現(xiàn)了 generator,其實(shí) await 就是 generator 加上 Promise的語(yǔ)法糖,且內(nèi)部實(shí)現(xiàn)了自動(dòng)執(zhí)行 generator。如果你熟悉 co 的話,其實(shí)自己就可以實(shí)現(xiàn)這樣的語(yǔ)法糖。

最后

  1. 只看不點(diǎn)贊就是耍流氓!!!
  2. 歡迎關(guān)注公眾號(hào)「前端進(jìn)階課」認(rèn)真學(xué)前端,一起進(jìn)階。
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容