阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

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

  • 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:1710071

评论 1 条评论

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

技术分享|GrowingIO分析云对ClickHouse的实践

Geek_2d6073

并发编程-CompletableFuture解析 | 京东物流技术团队

京东科技开发者

并发编程 CompletableFuture JDK1.8 企业号 7 月 PK 榜

【7.21-7.28】写作社区优秀技术博文一览

InfoQ写作社区官方

热门活动 优质创作周报

暑期参加百度网盘AI大赛,夺万元现金、获大厂内推!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

图解MySQL中SQL语句的执行过程

程序员小毕

Java MySQL 数据库 sql 程序员

LED显示屏分为几类,特点分别是什么?

Dylan

LED显示屏 户外LED显示屏 户内led显示屏

亚信安慧通过ISO20000认证,AntDB数据库团队服务能力再上新台阶

亚信AntDB数据库

数据库 AntDB AntDB数据库 企业号 7 月 PK 榜

如何开发一对一视频源码

山东布谷网络科技

App 源代码

【好文推荐】敏捷绩效考核如何做?

ShineScrum捷行

【实践篇】推荐算法PaaS化探索与实践 | 京东云技术团队

京东科技开发者

PaaS 推荐算法 PaaS平台化能力 企业号 7 月 PK 榜

数据库优化器设计穿越探索之旅

阿里技术

数据库 架构

Linux系统Memcached性能优化详细教程。

百度搜索:蓝易云

memcached 云计算 Linux 运维 云服务器

MegEngine Python 层模块串讲(中)

MegEngineBot

Python 深度学习 开源

技术优化:降本增效的常规实践

有态度的马甲

Apache Doris 1.2.6 版本正式发布|版本通告

SelectDB

数据库 大数据 后端 Doris

PoseiSwap 即将开启质押,利好刺激下 POSE通证短时涨超 30%

西柚子

HDC.Together2023 HarmonyOS学生公开课议程抢先看!

HarmonyOS开发者

HarmonyOS

Java 命令行参数解析方式探索(四):Spark & Flink

冰心的小屋

Java spark 命令行 command Parameter

电子科技大学入驻飞桨AI Studio高校专区,AI优质课程等你来学!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

防范地质灾害,北斗用芯监测

江湖老铁

Linux系统Docker优化详细教程。

百度搜索:蓝易云

Docker 云计算 Linux 运维 云服务器

如何基于 Apache Doris 构建新一代日志分析平台

SelectDB

数据库 大数据 数据分析 Doris

河北幸福消费金融基于 Apache Doris 构建实时数仓,查询提速 400 倍!

SelectDB

数据库 大数据 数据分析 后端 Doris

浅析 TiSpark v3.x 新变化

TiDB 社区干货传送门

版本测评 新版本/特性解读 7.x 实践

【落下帷幕】2023 中国大学生计算机设计大赛大数据应用大类国赛评审

ModelWhale

云计算 数据分析 在线编程 数据科学竞赛 中国大学生计算机设计大赛

软件测试/测试开发丨Python 内置库 OS 学习笔记分享

测试人

Python 软件测试 测试开发 os内置库

软件测试/测试开发丨Python 内置库 sys 学习笔记分享

测试人

Python 程序员 软件测试

PoseiSwap 即将开启质押,利好刺激下 POSE通证短时涨超 30%

大瞿科技

瀚元科技:利用A-OPS 智能运维助力边缘服务器运维效率提升30%

openEuler

Linux 运维 操作系统 openEuler 边缘

UPS设备在物流机房中的应用浅析 | 京东物流技术团队

京东科技开发者

机房管理 企业号 7 月 PK 榜 UPS

区块链服务网络的顶层设计与应用实践

BSN研习社

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