AICon 上海站|日程100%上线,解锁Al未来! 了解详情
写点什么

大厂前端高频面试问题与答案精选

  • 2019-02-21
  • 本文字数:5980 字

    阅读完需:约 20 分钟

大厂前端高频面试问题与答案精选

近日,GitHub 上一位名为木易杨(yygmind)的开发者,在 GitHub 中建了一个名为 Advanced-Frontend/Daily-Interview-Question 项目,该项目每天会更新一道前端大厂面试题,并邀请开发者在 issue 区中作答,以下是我们从该项目中挑选的 9 道题和答案,希望能给大家一些帮助。


GitHub 链接:


https://github.com/Advanced-Frontend/Daily-Interview-Question

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    }  }
复制代码


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1

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。



本题链接:


https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/4

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('防抖成功');    }
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));
复制代码


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5

4.介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set


  • 成员唯一、无序且不重复;

  • [value, value],键值与键名是一致的(或者说只有键值,没有键名);

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


WeakSet


  • 成员都是对象;

  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;

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


Map


  • 本质上是键值对的集合,类似集合;

  • 可以遍历,方法很多,可以跟各种数据格式转换。


WeakMap


  • 只接受对象最为键名(null 除外),不接受其他类型的值作为键名;

  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;

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


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/6

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
复制代码


测试成功。


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/9

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

复制代码


题目的本质,就是考察setTimeoutpromiseasync await的实现及执行顺序,以及 JS 的事件循环的相关问题。


答案:


script startasync1 startasync2promise1script endasync1 endpromise2settimeout
复制代码


过程详解链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7

7.将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组

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


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/8

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 = 0let 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 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来

  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码;

  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10


上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/11

9.谈谈你对 TCP 三次握手和四次挥手的理解


本题链接:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/15


更多内容,请关注前端之巅。



会议推荐


2019 年 6 月,GMTC 全球大前端技术大会 2019 即将到来。小程序、Flutter、移动 AI、工程化、性能优化…大前端的下一站在哪里?点击下图了解更多详情。



2019-02-21 10:1710255

评论 1 条评论

发布
用户头像
2019-11-07 21:55
回复
没有更多了
发现更多内容

2020了,Android开发是否真的还有出路!25岁的我还有机会吗

android 程序员 移动开发

2021 提升Android开发效率的实战技巧,女生学移动应用开发

android 程序员 移动开发

2020上半年百度Android岗(初级到高级)面试真题全收录

android 程序员 移动开发

2020个人开发者做一款Android-App需要知道的事情,年薪百万在此一举(1)

android 程序员 移动开发

2020关于面试字节跳动,我总结一些面试点,希望对最近需要面试的你们一些帮助

android 程序员 移动开发

2020最后一天! 我为大家准备一份Android 面试知识点大全迎接2021新的一年

android 程序员 移动开发

2020京东最新Android面试真题解析,kotlinarrow库

android 程序员 移动开发

2020年12月大厂BATJ面试ing-本以为学了个好找工作的Android开发,没想到又是坑

android 程序员 移动开发

2020年Android开发年终总结之如何挤进一线大厂?,BAT这种大厂履历意味着什么

android 程序员 移动开发

2020年腾讯丶百度丶字节丶OPPO等Android面试大全,附带教你如何写好简历

android 程序员 移动开发

2020抖音短视频爆火!它的背后到底是什么—,手把手教你写Android项目文档

android 程序员 移动开发

2021 最新Android常见知识体系,HR:,Android进程管理

android 程序员 移动开发

2020京东Android岗面试题大全(附赠京东内部真题解析PDF)

android 程序员 移动开发

架构设计七 如何设计异地多活架构

nydia

2020最全的BAT大厂面试题整理改版 (2),小程序开发

android 程序员 移动开发

2020每一位Android开发者应该知道,Android体系架构和开发库,没有干货你打我

android 程序员 移动开发

2020这一年的Android面经汇总(百度、腾讯、滴滴,移动端跨平台开发方案

android 程序员 移动开发

王者荣耀商城异地多活架构设计

Sky

「架构实战营」

2020字节跳动安卓程序员视频面试,这五点一定有助你顺利拿到offer(1)

android 移动开发

2020年失业后我整理了一份系统的Android面试题(含答案)

android 程序员 移动开发

2020年度总结:如果系统的Android学习可以这么简单!为什么不来看看呢

android 程序员 移动开发

2020年阿里巴巴Android面经:拿到字节跳动offer后,简历又被阿里捞了起来

android 程序员 移动开发

2020应届毕业生,Android春招总结,已入职小米(1),kotlin安卓开发教程

android 程序员 移动开发

2020最新GitHub-上-10-个顶级开源项目,2021最新大厂Android面试集合

android 程序员 移动开发

2021年之Android面经分享(已获头条、顺丰,html5移动端

android 程序员 移动开发

2021应届秋招:提前批挂后,二次面试字节跳动抖音Android客户端

android 程序员 移动开发

2020了,Android开发是否真的还有出路!25岁的我还有机会吗(1)

android 程序员 移动开发

2020字节跳动安卓程序员视频面试,这五点一定有助你顺利拿到offer

android 程序员 移动开发

2020应届毕业生,Android春招总结,已入职小米,阿里牛逼

android 程序员 移动开发

2020新一波跳槽季过后,Android程序员精选,大厂,flutter微信小程序

android 程序员 移动开发

2020荒诞的一年,35岁程序员现状:我现在房贷车贷家庭,android游戏开发大全

android 程序员 移动开发

大厂前端高频面试问题与答案精选_大前端_yygmind_InfoQ精选文章