高階前端程式設計師面試問題與答案【精選9道】

智雲程式設計發表於2019-03-07
高階前端程式設計師面試問題與答案【精選9道】

1. 寫 React/Vue 專案時為什麼要在元件中寫 key,其作用是什麼?

key 的作用是為了在 diff 演算法執行時更快的找到對應的節點,提高 diff 速度。

vue 和 react 都是採用 diff 演算法來對比新舊虛擬節點,從而更新節點。在 vue 的 diff 函式中。可以先了解一下 diff 演算法。

在交叉對比的時候,當新節點跟舊節點頭尾交叉對比沒有結果的時候,會根據新節點的 key 去對比舊節點陣列中的 key,從而找到相應舊節點(這裡對應的是一個 key => index 的 map 對映)。如果沒找到就認為是一個新增節點。而如果沒有 key,那麼就會採用一種遍歷查詢的方式去找到對應的舊節點。一種一個 map 對映,另一種是遍歷查詢。相比而言。map 對映的速度更快。

vue 部分原始碼如下:

// vue 專案  src/core/vdom/patch.js  -488 行
// oldCh 是一箇舊虛擬節點陣列, 
 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

建立 map 函式:

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 是對比新舊節點是否相同的函式
 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
    }
  }

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

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

首先讓我們回顧一下,map 函式的第一個引數 callback:

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

這個 callback 一共可以接收三個引數,其中第一個引數代表當前被處理的元素,而第二個引數代表該元素的索引。

  • 而 parseInt 則是用來解析字串的,使字串成為指定基數的整數。

parseInt(string, radix) 接收兩個引數,第一個表示被處理的值(字串),第二個表示為解析時的基數。

  • 瞭解這兩個函式後,我們可以模擬一下執行情況
  1. parseInt(‘1’, 0) //radix 為 0 時,且 string 引數不以“0x”和“0”開頭時,按照 10 為基數處理。這個時候返回 1;

  2. parseInt(‘2’, 1) // 基數為 1(1 進位制)表示的數中,最大值小於 2,所以無法解析,返回 NaN;

  3. parseInt(‘3’, 2) // 基數為 2(2 進位制)表示的數中,最大值小於 3,所以無法解析,返回 NaN。

  1. 什麼是防抖和節流?有什麼區別?如何實現?

防抖

觸發高頻事件後 n 秒內函式只會執行一次,如果 n 秒內高頻事件再次被觸發,則重新計算時間;

思路:

每次觸發事件時都取消之前的延時呼叫方法:

function debounce(fn) {
      let timeout = null; // 建立一個標記用來存放定時器的返回值
      return function () {
        clearTimeout(timeout); // 每當使用者輸入的時候把前一個 setTimeout clear 掉
        timeout = setTimeout(() => { // 然後又建立一個新的 setTimeout, 這樣就能保證輸入字元後的 interval 間隔內如果還有字元輸入的話,就不會執行 fn 函式
          fn.apply(this, arguments);
        }, 500);
      };
    }
    function sayHi() {
      console.log('防抖成功');
    }
    var inp = document.getElementById('inp');
    inp.addEventListener('input', debounce(sayHi)); // 防抖

2. 節流

高頻事件觸發,但在 n 秒內只會執行一次,所以節流會稀釋函式的執行頻率。

思路:

每次觸發事件時都判斷當前是否有等待執行的延時函式。

function throttle(fn) {
      let canRun = true; // 透過閉包儲存一個標記
      return function () {
        if (!canRun) return; // 在函式開頭判斷標記是否為 true,不為 true 則 return
        canRun = false; // 立即設定為 false
        setTimeout(() => { // 將外部傳入的函式的執行放在 setTimeout 中
          fn.apply(this, arguments);
          // 最後在 setTimeout 執行完畢後再把標記設定為 true(關鍵) 表示可以執行下一次迴圈了。當定時器沒有執行的時候標記永遠是 false,在開頭被 return 掉
          canRun = true;
        }, 500);
      };
    }
    function sayHi(e) {
      console.log(e.target.innerWidth, e.target.innerHeight);
    }
    window.addEventListener('resize', throttle(sayHi));

4. 介紹下 Set、Map、WeakSet 和 WeakMap 的區別?

Set

成員唯一、無序且不重複;

[value, value],鍵值與鍵名是一致的(或者說只有鍵值,沒有鍵名);

可以遍歷,方法有:add、delete、has。

WeakSet

成員都是物件;

成員都是弱引用,可以被垃圾回收機制回收,可以用來儲存 DOM 節點,不容易造成記憶體洩漏;

不能遍歷,方法有 add、delete、has。

Map

本質上是鍵值對的集合,類似集合;

可以遍歷,方法很多,可以跟各種資料格式轉換。

WeakMap

只接受物件最為鍵名(null 除外),不接受其他型別的值作為鍵名;

鍵名是弱引用,鍵值可以是任意的,鍵名所指向的物件可以被垃圾回收,此時鍵名是無效的;

不能遍歷,方法有 get、set、has、delete。

5. 介紹下深度優先遍歷和廣度優先遍歷,如何實現?

深度優先遍歷(DFS)

深度優先遍歷(Depth-First-Search),是搜尋演算法的一種,它沿著樹的深度遍歷樹的節點,儘可能深地搜尋樹的分支。當節點 v 的所有邊都已被探尋過,將回溯到發現節點 v 的那條邊的起始節點。這一過程一直進行到已探尋源節點到其他所有節點為止,如果還有未被發現的節點,則選擇其中一個未被發現的節點為源節點並重復以上操作,直到所有節點都被探尋完成。

簡單的說,DFS 就是從圖中的一個節點開始追溯,直到最後一個節點,然後回溯,繼續追溯下一條路徑,直到到達所有的節點,如此往復,直到沒有路徑為止。

DFS 可以產生相應圖的拓撲排序表,利用拓撲排序表可以解決很多問題,例如最大路徑問題。一般用堆資料結構來輔助實現 DFS 演算法。

注意:深度 DFS 屬於盲目搜尋,無法保證搜尋到的路徑為最短路徑,也不是在搜尋特定的路徑,而是透過搜尋來檢視圖中有哪些路徑可以選擇。

步驟:

訪問頂點 v;

依次從 v 的未被訪問的鄰接點出發,對圖進行深度優先遍歷;直至圖中和 v 有路徑相通的頂點都被訪問;

若此時途中尚有頂點未被訪問,則從一個未被訪問的頂點出發,重新進行深度優先遍歷,直到所有頂點均被訪問過為止。

實現:

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

測試:

graph.dfs()
// 1
// 4
// 3
// 2
// 5

測試成功。

廣度優先遍歷(BFS)

廣度優先遍歷(Breadth-First-Search)是從根節點開始,沿著圖的寬度遍歷節點,如果所有節點均被訪問過,則演算法終止,BFS 同樣屬於盲目搜尋,一般用佇列資料結構來輔助實現 BFS。

BFS 從一個節點開始,嘗試訪問儘可能靠近它的目標節點。本質上這種遍歷在圖上是逐層移動的,首先檢查最靠近第一個節點的層,再逐漸向下移動到離起始節點最遠的層。

步驟:

建立一個佇列,並將開始節點放入佇列中;

若佇列非空,則從佇列中取出第一個節點,並檢測它是否為目標節點;

若是目標節點,則結束搜尋,並返回結果;
若不是,則將它所有沒有被檢測過的位元組點都加入佇列中;
若佇列為空,表示圖中並沒有目標節點,則結束遍歷。

實現:

Graph.prototype.bfs = function(v) {
    var queue = [], marked = []
    marked[v] = true
    queue.push(v) // 新增到隊尾
    while(queue.length > 0) {
        var s = queue.shift() // 從隊首移除
        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)
            }
        }
    }
}

測試:

graph.bfs(1)
// visited vertex:  1
// visited vertex:  4
// visited vertex:  3
// visited vertex:  2
// visited vertex:  5

測試成功。

6. 非同步筆試題

請寫出下面程式碼的執行結果:

// 今日頭條面試題
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')

題目的本質,就是考察setTimeout、promise、async await的實現及執行順序,以及 JS 的事件迴圈的相關問題。

答案:

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

7. 將陣列扁平化並去除其中重複資料,最終得到一個升序且不重複的陣列

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

8.JS 非同步解決方案的發展歷程以及優缺點。

  1. 回撥函式(callback)
setTimeout(() => {
    // callback 函式體
}, 1000)

缺點:回撥地獄,不能用 try catch 捕獲錯誤,不能 return

回撥地獄的根本問題在於:

缺乏順序性: 回撥地獄導致的除錯困難,和大腦的思維方式不符;

巢狀函式存在耦合性,一旦有所改動,就會牽一髮而動全身,即(控制反轉);

巢狀函式過多的多話,很難處理錯誤。

ajax('XXX1', () => {
    // callback 函式體
    ajax('XXX2', () => {
        // callback 函式體
        ajax('XXX3', () => {
            // callback 函式體
        })
    })
})

優點:解決了同步的問題(只要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行)。

2. Promise

Promise 就是為了解決 callback 的問題而產生的。

Promise 實現了鏈式呼叫,也就是說每次 then 後返回的都是一個全新 Promise,如果我們在 then 中 return ,return 的結果會被 Promise.resolve() 包裝。

優點:解決了回撥地獄的問題。

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

缺點:無法取消 Promise ,錯誤需要透過回撥函式來捕獲。

3. Generator

特點:可以控制函式的執行,可以配合 co 函式庫使用。

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()

4. Async/await

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

優點是:程式碼清晰,不用像 Promise 寫一大堆 then 鏈,處理了回撥地獄的問題;

缺點:await 將非同步程式碼改造成同步程式碼,如果多個非同步操作沒有依賴性而使用 await 會導致效能上的降低。

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

下面來看一個使用 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

對於以上程式碼你可能會有疑惑,讓我來解釋下原因:

首先函式 b 先執行,在執行到 await 10 之前變數 a 還是 0,因為 await 內部實現了 generator ,generator 會保留堆疊中東西,所以這時候 a = 0 被儲存了下來;

因為 await 是非同步操作,後來的表示式不返回 Promise 的話,就會包裝成 Promise.reslove(返回值),然後會去執行函式外的同步程式碼;

同步程式碼執行完畢後開始執行非同步程式碼,將儲存下來的值拿出來使用,這時候 a = 0 + 10。

上述解釋中提到了 await 內部實現了 generator,其實 await 就是 generator 加上 Promise的語法糖,且內部實現了自動執行 generator。如果你熟悉 co 的話,其實自己就可以實現這樣的語法糖。

9. 談談你對 TCP 三次握手和四次揮手的理解

高階前端程式設計師面試問題與答案【精選9道】

如果你依然在程式設計的世界裡迷茫,不知道自己的未來規劃,可以加入web前端學習交流群:784783012 裡面可以與大神一起交流並走出迷茫。新手可免費領取學習資料,看看前輩們是如何在程式設計的世界裡傲然前行不停更新最新的教程和學習方法(詳細的前端專案實戰教學影片),有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入

點選: 加入


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901074/viewspace-2637849/,如需轉載,請註明出處,否則將追究法律責任。

相關文章