首頁 > 頭條 > 主體

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

發布: 2020-09-01 16:27:16
轉載
17201 人瀏覽過

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

【相關推薦:前端面試題(2020)】

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)接收兩個參數,第一個表示被處理的值(字串),第二個表示為解析時的基數。

了解這兩個函數後,我們可以模擬一下運行情況;

parseInt('1', 0)  //radix 為0 時,且string 參數不以「0x」和「0」開頭時,依照10 為基數處理。這時候回傳1;

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

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

map 函數傳回的是一個數組,所以最後結果為 [1, NaN, NaN]。

3. 什麼是防手震和節流?有什麼區別?如何實現?

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(&#39;防抖成功&#39;);
   }

   var inp = document.getElementById(&#39;inp&#39;);
   inp.addEventListener(&#39;input&#39;, 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(&#39;resize&#39;, throttle(sayHi));
登入後複製

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

1)Set

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

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

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

2)WeakSet

成員都是物件;

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

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

3)Map

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

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

4)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(&#39;visited vertex: &#39;, 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(&#39;async1 start&#39;)
   await async2()
   console.log(&#39;async1 end&#39;)
}
async function async2() {
   console.log(&#39;async2&#39;)
}
console.log(&#39;script start&#39;)
setTimeout(function () {
   console.log(&#39;settimeout&#39;)
})
async1()
new Promise(function (resolve) {
   console.log(&#39;promise1&#39;)
   resolve()
}).then(function () {
   console.log(&#39;promise2&#39;)
})
console.log(&#39;script end&#39;)
登入後複製

题目的本质,就是考察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(&#39;XXX1&#39;, () => {
   // callback 函数体
   ajax(&#39;XXX2&#39;, () => {
       // callback 函数体
       ajax(&#39;XXX3&#39;, () => {
           // callback 函数体
       })
   })
})
登入後複製

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)。

2)Promise

Promise 就是为了解决 callback 的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装。

优点:解决了回调地狱的问题。

ajax(&#39;XXX1&#39;)
 .then(res => {
     // 操作逻辑
     return ajax(&#39;XXX2&#39;)
 }).then(res => {
     // 操作逻辑
     return ajax(&#39;XXX3&#39;)
 }).then(res => {
     // 操作逻辑
 })
登入後複製

缺点:无法取消 Promise ,错误需要通过回调函数来捕获。

3)Generator

特点:可以控制函数的执行,可以配合 co 函数库使用。

function *fetch() {
   yield ajax(&#39;XXX1&#39;, () => {})
   yield ajax(&#39;XXX2&#39;, () => {})
   yield ajax(&#39;XXX3&#39;, () => {})
}
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(&#39;XXX1&#39;)
 await fetch(&#39;XXX2&#39;)
 await fetch(&#39;XXX3&#39;)
}
登入後複製

下面来看一个使用 await 的例子:

let a = 0
let b = async () => {
 a = a + await 10
 console.log(&#39;2&#39;, a) // -> &#39;2&#39; 10
}
b()
a++
console.log(&#39;1&#39;, a) // -> &#39;1&#39; 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 三次握手和四次揮手的理解

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

相關標籤:
來源:前端大学
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板