Advanced-Frontend Interview-question

257 阅读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){
    
}

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


3.什么是防抖和节流?有什么区别?如何实现?

  • 防抖

触发高频事件后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.addEventLisenter('input', debounce(sayHi)); //防抖
  • 节流

高频事件触发,但在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 = edge.get(u) //邻居节点
        for (var i = 0; i< neighbors.length; i++){
            var w = neighbors[i]
            if(!marked[w]) {
                dfsVisit (w)
            }
        }
    }
}
  • 广优先遍历(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 vertext:', 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)
            }
        }
    }
}

6,异步笔试题

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 函数体 }) }) })

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

  1. 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 ,错误需要通过回调函数来捕获。

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

  1. 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三次握手 和 四次挥手理解