大廠前端經(jīng)典面試問題精選(附答案)

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

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

vue 和 react 都是采用 diff 算法來對(duì)比新舊虛擬節(jié)點(diǎn),從而更新節(jié)點(diǎn)。在 vue 的 diff 函數(shù)中。可以先了解一下 diff 算法。

在交叉對(duì)比的時(shí)候,當(dāng)新節(jié)點(diǎn)跟舊節(jié)點(diǎn)頭尾交叉對(duì)比沒有結(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 映射)。如果沒找到就認(rèn)為是一個(gè)新增節(jié)點(diǎn)。而如果沒有 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

? }

}

2. 解析 ['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 則是用來解析字符串的,使字符串成為指定基數(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”開頭時(shí),按照 10 為基數(shù)處理。這個(gè)時(shí)候返回 1;

parseInt('2', 1) // 基數(shù)為 1(1 進(jìn)制)表示的數(shù)中,最大值小于 2,所以無法解析,返回 NaN;

parseInt('3', 2) // 基數(shù)為 2(2 進(jìn)制)表示的數(shù)中,最大值小于 3,所以無法解析,返回 NaN。

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

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

1)防抖

觸發(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)記用來存放定時(shí)器的返回值

? ? return function () {

? ? ? clearTimeout(timeout); // 每當(dāng)用戶輸入的時(shí)候把前一個(gè) setTimeout clear 掉


? ? ? // 然后又創(chuàng)建一個(gè)新的 setTimeout, 這樣就能保證輸入字符后的 interval 間隔內(nèi)如果還有字符輸入的話,

? ? ? //就不會(huì)執(zhí)行 fn 函數(shù)

? ? ? timeout = setTimeout(() => {

? ? ? ? fn.apply(this, arguments);

? ? ? }, 500);

? ? };

? }

? function sayHi() {

? ? console.log('防抖成功');

? }

? var inp = document.getElementById('inp');

? inp.addEventListener('input', debounce(sayHi)); // 防抖

2)節(jié)流

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

思路:

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

function throttle(fn) {

? ? let canRun = true; // 通過閉包保存一個(gè)標(biāo)記

? ? return function () {

? ? ? if (!canRun) return; // 在函數(shù)開頭判斷標(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í)器沒有執(zhí)行的時(shí)候標(biāo)記永遠(yuǎn)是 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 的區(qū)別?

1)Set

成員唯一、無序且不重復(fù);

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

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

2)WeakSet

成員都是對(duì)象;

成員都是弱引用,可以被垃圾回收機(jī)制回收,可以用來保存 DOM 節(jié)點(diǎn),不容易造成內(nèi)存泄漏;

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

3)Map

本質(zhì)上是鍵值對(duì)的集合,類似集合;

可以遍歷,方法很多,可以跟各種數(shù)據(jù)格式轉(zhuǎn)換。

4)WeakMap

只接受對(duì)象最為鍵名(null 除外),不接受其他類型的值作為鍵名;

鍵名是弱引用,鍵值可以是任意的,鍵名所指向的對(duì)象可以被垃圾回收,此時(shí)鍵名是無效的;

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

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

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

深度優(yōu)先遍歷(Depth-First-Search),是搜索算法的一種,它沿著樹的深度遍歷樹的節(jié)點(diǎn),盡可能深地搜索樹的分支。當(dāng)節(jié)點(diǎn) v 的所有邊都已被探尋過,將回溯到發(fā)現(xiàn)節(jié)點(diǎn) v 的那條邊的起始節(jié)點(diǎn)。這一過程一直進(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)單的說,DFS 就是從圖中的一個(gè)節(jié)點(diǎn)開始追溯,直到最后一個(gè)節(jié)點(diǎn),然后回溯,繼續(xù)追溯下一條路徑,直到到達(dá)所有的節(jié)點(diǎn),如此往復(fù),直到?jīng)]有路徑為止。

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

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

步驟:

訪問頂點(diǎn) v;

依次從 v 的未被訪問的鄰接點(diǎn)出發(fā),對(duì)圖進(jìn)行深度優(yōu)先遍歷;直至圖中和 v 有路徑相通的頂點(diǎn)都被訪問;

若此時(shí)途中尚有頂點(diǎn)未被訪問,則從一個(gè)未被訪問的頂點(diǎn)出發(fā),重新進(jìn)行深度優(yōu)先遍歷,直到所有頂點(diǎn)均被訪問過為止。

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

? ? ? ? ? }

? ? ? }

? }

}

測(cè)試:

graph.dfs()

// 1

// 4

// 3

// 2

// 5

測(cè)試成功。

廣度優(yōu)先遍歷(BFS)

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

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

步驟:

創(chuàng)建一個(gè)隊(duì)列,并將開始節(jié)點(diǎn)放入隊(duì)列中;

若隊(duì)列非空,則從隊(duì)列中取出第一個(gè)節(jié)點(diǎn),并檢測(cè)它是否為目標(biāo)節(jié)點(diǎn);

若是目標(biāo)節(jié)點(diǎn),則結(jié)束搜尋,并返回結(jié)果;

若不是,則將它所有沒有被檢測(cè)過的字節(jié)點(diǎn)都加入隊(duì)列中;

若隊(duì)列為空,表示圖中并沒有目標(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)

? ? ? ? ? }

? ? ? }

? }

}

測(cè)試:

graph.bfs(1)

// visited vertex:? 1

// visited vertex:? 4

// visited vertex:? 3

// visited vertex:? 2

// visited vertex:? 5

測(cè)試成功。

6. 異步筆試題

請(qǐng)寫出下面代碼的運(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)問題。

答案:

script start

async1 start

async2

promise1

script end

async1 end

promise2

settimeout

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

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

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

1)回調(diào)函數(shù)(callback)

setTimeout(() => {

? // callback 函數(shù)體

}, 1000)

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

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

缺乏順序性: 回調(diào)地獄導(dǎo)致的調(diào)試?yán)щy,和大腦的思維方式不符;

嵌套函數(shù)存在耦合性,一旦有所改動(dòng),就會(huì)牽一發(fā)而動(dòng)全身,即(控制反轉(zhuǎn));

嵌套函數(shù)過多的多話,很難處理錯(cuò)誤。

ajax('XXX1', () => {

? // callback 函數(shù)體

? ajax('XXX2', () => {

? ? ? // callback 函數(shù)體

? ? ? ajax('XXX3', () => {

? ? ? ? ? // callback 函數(shù)體

? ? ? })

? })

})

優(yōu)點(diǎn):解決了同步的問題(只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行)。

2)Promise

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

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

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

ajax('XXX1')

.then(res => {

? ? // 操作邏輯

? ? return ajax('XXX2')

}).then(res => {

? ? // 操作邏輯

? ? return ajax('XXX3')

}).then(res => {

? ? // 操作邏輯

})

缺點(diǎn):無法取消 Promise ,錯(cuò)誤需要通過回調(diào)函數(shù)來捕獲。

3)Generator

特點(diǎn):可以控制函數(shù)的執(zhí)行,可以配合 co 函數(shù)庫(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()

4)Async/await

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

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

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

async function test() {

// 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式

// 如果有依賴性的話,其實(shí)就是解決回調(diào)地獄的例子了

await fetch('XXX1')

await fetch('XXX2')

await fetch('XXX3')

}

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

對(duì)于以上代碼你可能會(huì)有疑惑,讓我來解釋下原因:

首先函數(shù) b 先執(zhí)行,在執(zhí)行到 await 10 之前變量 a 還是 0,因?yàn)?await 內(nèi)部實(shí)現(xiàn)了 generator ,generator 會(huì)保留堆棧中東西,所以這時(shí)候 a = 0 被保存了下來;

因?yàn)?await 是異步操作,后來的表達(dá)式不返回 Promise 的話,就會(huì)包裝成 Promise.reslove(返回值),然后會(huì)去執(zhí)行函數(shù)外的同步代碼;

同步代碼執(zhí)行完畢后開始執(zhí)行異步代碼,將保存下來的值拿出來使用,這時(shí)候 a = 0 + 10。

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

9. 談?wù)勀銓?duì) TCP 三次握手和四次揮手的理解

更多web前端開發(fā)知識(shí),請(qǐng)查閱 HTML中文網(wǎng) !!

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • ## 框架和庫(kù)的區(qū)別?> 框架(framework):一套完整的軟件設(shè)計(jì)架構(gòu)和**解決方案**。> > 庫(kù)(lib...
    Rui_bdad閱讀 3,151評(píng)論 1 4
  • "use strict";function _classCallCheck(e,t){if(!(e instanc...
    久些閱讀 2,142評(píng)論 0 2
  • 如何控制alert中的換行?\n alert(“p\np”); 請(qǐng)編寫一個(gè)JavaScript函數(shù) parseQu...
    heyunqiang99閱讀 1,140評(píng)論 0 6
  • 單例模式 適用場(chǎng)景:可能會(huì)在場(chǎng)景中使用到對(duì)象,但只有一個(gè)實(shí)例,加載時(shí)并不主動(dòng)創(chuàng)建,需要時(shí)才創(chuàng)建 最常見的單例模式,...
    Obeing閱讀 2,314評(píng)論 1 10
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,879評(píng)論 0 5

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