近日恢共,GitHub 上一位名為木易楊(yygmind)的開發(fā)者诬垂,在 GitHub 中建了一個(gè)名為 Advanced-Frontend/Daily-Interview-Question 的項(xiàng)目瘩燥,該項(xiàng)目每天會(huì)更新一道大廠前端面試題,并邀請開發(fā)者在 issue 區(qū)中作答不皆,以下是從該項(xiàng)目中挑選的 9 道題和答案蟆技。
1. 寫 React/Vue 項(xiàng)目時(shí)為什么要在組件中寫 key,其作用是什么沿侈?
key 的作用是為了在 diff 算法執(zhí)行時(shí)更快的找到對應(yīng)的節(jié)點(diǎn)闯第,提高 diff 速度。
vue 和 react 都是采用 diff 算法來對比新舊虛擬節(jié)點(diǎn)缀拭,從而更新節(jié)點(diǎn)咳短。在 vue 的 diff 函數(shù)中填帽。可以先了解一下 diff 算法咙好。
在交叉對比的時(shí)候篡腌,當(dāng)新節(jié)點(diǎn)跟舊節(jié)點(diǎn)頭尾交叉對比沒有結(jié)果的時(shí)候,會(huì)根據(jù)新節(jié)點(diǎn)的 key 去對比舊節(jié)點(diǎn)數(shù)組中的 key敷扫,從而找到相應(yīng)舊節(jié)點(diǎn)(這里對應(yīng)的是一個(gè) key => index 的 map 映射)哀蘑。如果沒找到就認(rèn)為是一個(gè)新增節(jié)點(diǎn)。而如果沒有 key葵第,那么就會(huì)采用一種遍歷查找的方式去找到對應(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 是對比新舊節(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]寸齐。
附上 MDN 上對于這兩個(gè)函數(shù)的鏈接,具體參數(shù)大家可以到里面看:parseInt | map
3. 什么是防抖和節(jié)流抄谐?有什么區(qū)別渺鹦?如何實(shí)現(xiàn)?
3.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 掉
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)); // 防抖
3.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ū)別募逞?
4.1 Set
- 成員唯一、無序且不重復(fù)馋评;
- [value, value]放接,鍵值與鍵名是一致的(或者說只有鍵值,沒有鍵名)留特;
- 可以遍歷纠脾,方法有:add、delete蜕青、has苟蹈。
4,2 WeakSet
- 成員都是對象;
- 成員都是弱引用右核,可以被垃圾回收機(jī)制回收慧脱,可以用來保存 DOM 節(jié)點(diǎn),不容易造成內(nèi)存泄漏贺喝;
- 不能遍歷菱鸥,方法有 add宗兼、delete、has氮采。
4.3 Map
- 本質(zhì)上是鍵值對的集合殷绍,類似集合;
- 可以遍歷鹊漠,方法很多主到,可以跟各種數(shù)據(jù)格式轉(zhuǎn)換。
4.4 WeakMap
- 只接受對象最為鍵名(null 除外)躯概,不接受其他類型的值作為鍵名登钥;
- 鍵名是弱引用,鍵值可以是任意的楞陷,鍵名所指向的對象可以被垃圾回收怔鳖,此時(shí)鍵名是無效的;
- 不能遍歷固蛾,方法有 get结执、set、has艾凯、delete献幔。
5. 介紹下深度優(yōu)先遍歷和廣度優(yōu)先遍歷,如何實(shí)現(xiàn)趾诗?
5.1 深度優(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)都被探尋完成却舀。
簡單的說,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ā)袍镀,對圖進(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)
}
}
}
}
測試:
graph.dfs()
// 1
// 4
// 3
// 2
// 5
測試成功。
5.2 廣度優(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),并檢測它是否為目標(biāo)節(jié)點(diǎn)浴韭;
- 若是目標(biāo)節(jié)點(diǎn)丘喻,則結(jié)束搜尋,并返回結(jié)果念颈;
- 若不是泉粉,則將它所有沒有被檢測過的字節(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)
}
}
}
}
測試:
graph.bfs(1)
// visited vertex: 1
// visited vertex: 4
// visited vertex: 3
// visited vertex: 2
// visited vertex: 5
測試成功。
6. 異步筆試題
請寫出下面代碼的運(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)蜜自。
8.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í)很長,后面的任務(wù)都必須排隊(duì)等著鸣皂,會(huì)拖延整個(gè)程序的執(zhí)行)抓谴。
8.2 Promise
Promise 就是為了解決 callback 的問題而產(chǎn)生的。
Promise 實(shí)現(xiàn)了鏈?zhǔn)秸{(diào)用寞缝,也就是說每次 then 后返回的都是一個(gè)全新 Promise癌压,如果我們在 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ù)來捕獲被啼。
8.3 Generator
特點(diǎn):可以控制函數(shù)的執(zhí)行帜消,可以配合 co 函數(shù)庫使用。
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()
8.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
對于以上代碼你可能會(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ù)勀銓?TCP 三次握手和四次揮手的理解
本題鏈接
小伙伴可以在公號(hào)【grain先森】后臺(tái)回復(fù)【190301】獲取130套簡歷模板摧冀。全部題目鏈接?? 戳這里