• Home
  • Archives
  Evan的博客
  • Home
  • Archives
  • 面试
  • 原理笔记
  • 项目实践
  • 其他

前端面经复盘

2020/10/11 posted in  面试

前言

这次面试总共花了约一个月,8 月中旬开始,到 9 月中旬

期间共面试 5 家公司:腾讯,字节跳动,百度,富途,转转

面试完之后打算抽空简单写写总结,复盘。这篇面经主要会针对以下几个方面去总结:

  1. 每个厂面试过程的感受

  2. 每个厂的面试风格和难度

  3. 印象深刻或者觉得有价值的真题

    不会写很详细的解答,会提及相关知识点,想了解的还是需要自己查一查资料。要是全部详细写完剖析完,怕是能写一小本子了...

  4. 最后会聊聊复习策略,选厂看法和未来的路等

题目较多,建议先把所有面经风格看完,再回过头看感兴趣的真题

腾讯面经

面试进度:offer

面试难度:⭐️⭐️⭐️⭐️

面试风格:深挖原理 + 适量算法

过程介绍:

我面的是 PCG 的部门。腾讯的面试流程很长,前前后后需要大概一个月。一个部门大概是4 轮技术面,1 轮 hr 面。可以换部门车轮战,但是真的要等很久很久...

整个面试过程感觉是比较 nice 的,每一轮面试官都很尊重人,hr 谈薪资 offer 之类的也是很和蔼(其实声音很年轻 2333)整体下来感觉面的很舒服,至少我面的部门给人感觉很舒服。

面试风格这一块腾讯比较喜欢深挖原理,只要写在简历上的,都会把你问个底朝天。建议没有十足把握的(我指的是有自己见解或者看过源码)项目或者技术不要写上,千万不要觉得看过一点甚至只是听过某个框架,就写上去...否则会死的很惨...算法问的不算多,谈不上难,刷过一定题量的同学应该问题不大。

面试题:

  1. Vue 双向绑定

    常规操作,但是会一直往深处问,除了对象劫持,Proxy 这些常规回答,还需要了解一下整个发布订阅的流程,Watcher,Dep,Observer,Compile 等几个类的源码建议读一下

  2. 有多少种不同类型的 Watcher

    data,props,computed,watch,以及 vuex 中 state 也是 Watcher

  3. Vue 什么时候收集 Watcher 的依赖

    所谓收集依赖意思就是问什么时候注入 Watcher,即绑定观察者和被观察者之间的关系。最好先介绍一下有哪几种 Watcher,再分类讨论不同 Watcher 的注入特性。我面的时候面试官是循序渐进的,看得出是源码大牛,一个个问题环环相扣。

    简单介绍一下 Vue 实例化的过程:首先劫持 VNode.prototype 变成可响应,然后有个执行 _init 操作。感兴趣的童鞋可以查一下 initMixin 这个方法。这个过程中会初始化生命周期,事件,渲染方法,然后在处理完 beforeCreated 的钩子之后,会执行 initState,就是在这玩意里面收集 data,props 以及 computed 相关的 Watcher 依赖。总之就是,在 created 之前,beforeCreated 之后,要处理完所有 vm 实例的依赖收集并且初始化,这也是为什么 created 就能用 this.xx 的原因。具体细节还是要自己看看源码,当然还有包括 watch 其实也是 Watcher,但是这时候就是开发者主动声明的观察者,而不是 Vue 初始化的时候帮我们分析依赖了。

  4. 父子组件嵌套的时候,Vue 第一次注入 Watcher 的时候是什么时候,为什么

    这个其实问的是组件初始化的顺序。父 created,子 created,子 mounted,父 mounted。类似 Koa 的洋葱模型,从外到里再到外。原因也比较简单,子组件未确认状态且未渲染的时候,父组件要是抢先确定了自己状态并且渲染,那不就裂开了,它怎么知道它的子组件长啥样。

  5. computed 的 Watcher 和 data 的 Watcher 有啥区别

    核心就是缓存,至于如何缓存的这里就不过多赘述了,一搜一大把。还是那句话,记得看源码。

  6. Vuex 的设计模式

    单例模式,单项数据流,状态追踪

  7. 为什么异步操作要写到 actions 里面,而不能异步 mutation

    mutation 会修改 state 数据,异步的 mutation 状态是无法追踪的。假如 mutation 是异步,比如请求数据,那不知道什么时候回调会有响应,也就无法得知 state 啥时候被修改。

    既然 commit 一个 mutation 必须是同步的,那就异步 dispatch action 好了,然后让 action 去 commit 同步的 mutation。dispatch 可以是异步的也没关系,啥时候回调回来也没关系,反正最终他会 commit,只需要追踪 commit 一个 mutation 的时间点的快照就好了,这样数据流仍然是可追踪的。

  8. Vuex 是什么时候注入 Vue 的,怎么注入

    一样的道理,既然能 this.xx 调用,那肯定要在 created 之前注入。其实 Vuex 本质只是一种数据流状态的管理方式,就算脱离了 Vue 也是能使用的。这和 Redux 类似。至于注入的过程,其实就是 mixin。当然这个过程涉及到 Vuex moudle 的组合和构建,Vuex 的源码我没看的很仔细,有时间补一补。

  9. Vue 作为 viewModel 层,它是怎么感知 model 层中状态的变更的

    这里我也是猜测。既然要响应式那还是离不开 getter 和 setter 两兄弟。Vuex 初始化完会在 Vue 实例(这里称它为 vmA)上挂载一个 $store,其实里面的数据就是利用一个 new Vue (这里称它为 vmB)来做发布订阅和双向绑定的。也就是说当读取(或设置)某个 state 的属性的时候,其实就是触发 vmB 的 getter setter 两兄弟,再通知 vmA 罢了。类似于自己利用 Vue 写个 $Bus 吧。(大部分自己理解,可能有点出入,不过后来稍微查了下资料大致是这个思路)

  10. 介绍一下 service worker

    web worker 的是一种,是独立的线程。介绍一下 sw 的生命周期,还有常见应用场景(pwa 和缓存,跨页面通信等。注意这里其实也会深挖,比如如何缓存 response 的,然后怎么控制缓存池的缓存策略,如何更新 sw 版本,兜底方案之类的)

  11. service worker 如何做到跨线程通信

    messageChannel 或者 postMessage 之类的。除了正常通讯,其实也能做心跳保活,一般用来侦测页面是否崩溃。(页面崩溃的时候,当前页面的 js 线程压根就不会工作,一切通信都无济于事。但是 sw 的独立的线程,他不受影响)比如 tabA 和 tabB 两个客户端,都接入 sw,然后做 messageChannel 或者 postMessage 心跳保活。一旦收不到某一端的心跳消息,就意味着这个 tab 挂了

  12. 既然是独立线程,那必然不是同步的,怎么保证消息传递的同步性,如何设计

    回调。关于设计,我也是即兴发挥,个人感觉可以参考 promise 状态机的设计理念,resolve 保证同步并且遵循 A+ 规范状态不可逆

  13. webpack plugin 有哪些生命周期钩子,可以用来做什么

    记一个大概流程即可:即将开始编译,开始编译,编译完成,准备生成文件,结束并释放资源

    1. Compile 开始进入编译环境,开始编译
    2. Compilation 即将产生第一个版本
    3. make 任务开始
    4. optimize 作为 Compilation 的回调方法,优化编译,在 Compilation 回调函数中可以为每一个新的编译绑定回调。
    5. after-compile编译完成
    6. emit准备生成文件,开始释放生成的资源,最后一次添加资源到资源集合的机会
    7. after-emit文件生成之后,编译器释放资源

    可以利用一些钩子扩充 webpack 的能力,比如构建前清空 output 目录,比如移动文件,编译完成后修改某些文件 Buffer 等

  14. 发布订阅和观察者模式的区别

    观察者没有中间商赚差价...

    实现方式,差异,优劣势等谷歌搜一下吧,这里不赘述了

  15. WebSocket

    双工通信,不受同源策略限制,二进制传输,由 http upgrade 等

    主要介绍一下和长链接有什么不同:

    长连接是 http 1.1 的规范。客户端发起一个 http 请求后,服务端保持请求状态而不响应,直到需要推送的时候再响应。因为 http 是一个 req 和 res 相对应的连接,所以每次服务端响应之后,http 断开,这时候客户端再发一个 http 过来,服务端保持请求状态。换句话说如果客户端不发 http 过来,就算是 tcp 连着,服务端推送消息客户端也收不到,因为不知道是客户端是谁

  16. http2

    1. 传输数据由明文传输改成二进制流传输
    2. 数据传输采用多路复用,请求合并在同一 TCP 连接内,解决队头阻塞的问题
    3. 支持服务端推送
    4. 使用 HPACK 算法来压缩首部内容
  17. quic

    改用 udp ,彻底干掉队头阻塞的问题。http2 归根结底还是 tcp 的,某个包丢了还是要等待重连。强依赖唯一一个 tcp 的策略,甚至在某些情况还不如 http1.1。比如这个 tcp 就是挂了,好歹 http1.1 还有其他的 tcp 连接数不至于全崩

    quic 注意他是通过 Stream Offset 来控制可靠性的,感兴趣的童鞋可以查一下相关资料,这里不多说了

  18. 手写 call 方法

    手写是一方面,感觉面试官更想了解函数设计思路。比如他提问,你会选择哪种调用方法来执行函数(挂载原型链还是声明独立函数)还有执行过程中一些细节,比如 delete 掉绑定在上下文上的方法再 return;比如上下文中会不会已经有了这个方法名,如何兼容等

  19. 算法

// 题一
// 类似 leetcode 673题
// 给一个字符串,找出最长递增子序列
// 如果有相同长度,返回字典集最小的
//
// 例:
// 输入 "23648179" 返回 "23489"

// 题二
// leetcode 206题
// 翻转链表

// 题三
// leetcode 94题
// 中序遍历一棵树,除了递归还能用什么方法
  1. 其他都是和项目相关的,或者个人感觉参考价值不大的,就不过多介绍了

和项目相关的就不说了

然后以下面试题和项目无关,但是感觉参考价值不大,感兴趣可以了解

WebRTC,生成和注入骨架屏,性能优化,Mutation Observer 加权计算首屏时间,NAT 穿越,docker 相关,serverless 相关,JavaScript Bridge 原理,flutter,rn,hybrid 三者原理和性能差距,线程和进程的区别,链表和树的区别

字节面经

面试进度:offer

面试难度:⭐️⭐️⭐️⭐️

面试风格:大量算法

过程介绍:

字节面的是小程序相关的部门。字节面试效率挺高,基本上 10 天左右能面完。3 轮技术面,1 轮 hr 面。面试感觉是雷厉风行的,很尊重面试者,但是也很干练,不会扯无关的东西。很注重效率。字节给的薪资是这次面试中所有厂里给的最高的,甚至超过了我预期不少...

面试风格这一块字节是名副其实的算法大户,每一轮都有 2-3 题算法,3 轮下来大概 8-9 题这样。难度比例大概是 15% 简单的题,70% 中等难度的题,15% 比较难的题。第三轮是压力面,会不断给你压力做一些比较难(或者边界条件比较多)的应用类算法,而且必须可执行通过提供的几个测试用例,加上时间限制,容易把心态搞崩...

除了刷题之外,要有意识去锻炼思维,比如什么时候用动态规划,什么时候尾递归,什么时候回溯剪枝,什么时候用栈等等。字节的算法面试和其他厂不一样,其他厂来来去去就那几十题常见的,字节可能就是从 leetcode 几千号题库里随便抽一道出来...任你刷题刷爆肝,想背题在字节这里是行不通的。

面试题:

  1. 写出输出结果

    var a = function () {
     this.b = 3
    }
    var c = new a()
    a.prototype.b = 9
    var b = 7
    a()
    console.log(b) // 7
    console.log(c.b) // 3
  2. 实现批量请求函数

    // 实现一个可以批量 fetch 的函数,接收 urls 数组,max 最大并发量,cb 回调三个参数
    // 当所有请求结束之后,需要执行cb
    // 始终保持最大并发量执行,即一个 fetch 结束,另一个 fetch 立即补上,直到请求完所有的 url
    /**
    * 批量请求函数,每个人写法不同,这里就不写了
    * @param {Array} urls api数组
    * @param {Number} max 最大并发量
    * @param {Function} callback 回调函数
    */
    const sendRequest = (urls, max, callback) => {
    // ...
    }
  3. 实现函数连续调用

    // 实现一个求和方法,支持以下调用方式
    // 1. sum(1)(2)(3)
    // 2. sum(1, 2)(3)
    // 3. sum(1, 2, 3)
    /**
    * 思路是柯里化函数
    */
    function sum() {
    let args = [].slice.call(arguments)
    let fn = function () {
    let fn_args = [].slice.call(arguments)
    return sum.apply(null, args.concat(fn_args))
    }
    fn.toString = function () {
    return args.reduce((a, b) => a + b)
    }
    return fn
    }
    const t1 = sum(1)(2)(3)
    const t2 = sum(1, 2)(3)
    const t3 = sum(1, 2, 3)
    console.log(t1.toString()) // 6
    console.log(t2.toString()) // 6
    console.log(t3.toString()) // 6
  4. 用尾递归实现一个阶乘方法

    // 简单粗暴的阶乘其实很简单,但是要考虑性能就涉尾递归
    // 然后面试官还问了尾递归是为了解决什么问题
    // 然后为什么 node 里现在反而又取消了尾递归的支持
    //
    // 尾递归是防止爆栈,原理是 js 运行时函数执行栈追踪环境相关的问题
    // 尾递归虽然防爆栈,但是函数执行环境的追踪也丢失了
    // 感兴趣可以查一下相关资料,这里要说又能说半天...
    /**
    * 暴力版阶乘
    * @param {Number} n 阶乘数
    */
    function fn(n) {
    if (n === 1) return 1
    return n * fn(n - 1)
    }
    /**
    * 尾递归阶乘
    * @param {Number} n 阶乘数
    * @param {Number} x 上一轮执行结果,初始值 1
    */
    function fn(n, x = 1) {
    if (n === 1) return x
    return fn(n - 1, n * x)
    }
  5. 二叉树路径求和

    // 给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径
    // 这条路径上所有节点值相加等于目标和。
    // leetcode 112题
    /**
    * 二叉树路径求和
    * @param {Node} root 根节点
    * @param {Number} sum 和
    */
    const hasPathSum = function (root, sum) {
    if (!root) return false
    if (root.left === null && root.right === null) return root.val === sum
    sum = sum - root.val
    return hasPathSum(root.left, sum) || hasPathSum(root.right, sum)
    }
  6. 大数相加

    // 相加还是相乘我忘了,面试过程中还有些题不太记得
    // 反正相加也好相乘也好,都是模拟小学学运算的时候逐位运算
    //
    // 这题我觉得比较有应用意义,一些大数的运算用 Number 型会溢出,需要用字符串模拟运算
    // 再加上网上搜到的结果五花八门,这里代码就详细写一下
    /**
    * 大数相加
    * @param {String} a 数字字符
    * @param {String} b 数字字符
    */
    function plus(a, b) {
    let maxLength = Math.max(a.length, b.length)
    a = a.padStart(maxLength, 0) // 补0
    b = b.padStart(maxLength, 0) // 补0
    let x = 0 // 位数相加的结果
    let y = 0 // 进位
    let sum = '' // 最终字符串
    for (let i = maxLength - 1; i >= 0; i--) {
    x = Number(a[i]) + Number(b[i]) + y // 位数相加,并且加上进位
    y = Math.floor(x / 10) // 进位
    sum = `${x % 10}${sum}` // 位数相加结果取余,拼接上已有结果
    }
    return y === 1 ? `1${sum}` : sum // 有多余的进位记得加上
    }
  7. 三数之和

    // leetcode 15题
    // 判断数组 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0
    /**
    * 三数求和
    * @param {Array} nums 数组
    */
    const threeSum = function (nums) {
    nums.sort((a, b) => a - b)
    let res = []
    for (let i = 0; i < nums.length; i++) {
    if (nums[i] * nums[nums.length - 1] > 0) return res
    if (i > 0 && nums[i] === nums[i - 1]) continue
    let lp = i + 1
    let rp = nums.length - 1
    while (lp < rp) {
    const sum = nums[i] + nums[lp] + nums[rp]
    if (sum === 0) {
    res.push([nums[i], nums[lp], nums[rp]])
    lp++
    rp--
    while (lp < rp && nums[lp] === nums[lp - 1]) lp++
    while (lp < rp && nums[rp] === nums[rp + 1]) rp--
    }
    if (sum > 0) rp--
    if (sum < 0) lp++
    }
    }
    return res
    }
  8. 实现一个计算器

    // leetcode 772题(224题 + 227题的结合体)
    // 这题真要搞起来是无敌蛋疼
    // 因为要考虑的元素很多,比如:
    // 输入的字符串是不是有效的运算式,多层级括号用栈处理优先级,4则运算的优先级等等
    // 这里我是懒得写了...网上也有答案
  9. Vue 和 React 你觉得哪个好,你做项目如何选型

    Vue 集成度更高,上手快且方便

    React 灵活性更强,方便根据团队和业务特性深度定制,比如 react 定制脚手架和 webpack 更方便,对整个 vm 层控制权更大,all in js。其他就 diff 上有些差异。选择的话优先考虑团队技术栈,二是对 build 包轻重量的考虑,三是考虑更偏向于通用形还是需要深度定制

  10. 介绍一下箭头函数的,以及它 this 的指向

    箭头函数没有上下文,this 继承于定义时的外层词法环境,如果没有则跟着作用域链一路查找直到最外层(浏览器是 window 对象,node 环境是 exports 对象)

    注意在 node 环境执行箭头函数 this 会指向 exports 对象而不是 global,因为 node 是模块化的,node 执行每个文件会给一个单独的作用域(模块),这是一个闭包环境。在一个模块(文件)中,声明的变量都是在这个闭包环境内的,不会污染 global ,而 this 指向的是 module.exports ,而不是 global

    如果直接调用一个正常定义的函数,那么这个就是由 global 对象调用的,this 会绑定在 global 上。如果调用一个箭头函数,this 是向上级词法环境查找的,如果上级没有作用域,就跟着作用域链一路找到定义时的最外层对象上(exports)

  11. 简单请求和复杂请求有什么区别,怎样算是复杂请求

    简单请求和复杂请求都是对于跨域而言的。满足以下条件是简单请求,其他都是复杂请求

    1. 是 get/post/head 其中一种请求
    2. 请求头只包含 accept,accept-language,content-language,content-type
    3. 并且 content-type 是表单,formdata 或者文本中的一种

    对于简单请求,浏览器会在请求头加一个 origin 字段,用来告诉服务端是否可以跨域。对于复杂请求,浏览器会发起一个 options 的预检信息,预检通过之后才会发正式请求,也是用 origin 判断

  12. 有哪些跨域的方法

    1. jsonp
    2. ng 配置 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 相关属性
    3. 后端服务器也可以设置允许跨域,自己设也行,或者用一些第三方封装好的包。比如 node 可以用 koa-cors 之类的包
  13. 如何检测内存泄漏

    内存快照

  14. V8 垃圾回收策略是怎样的

    新生代算法和老生代算法。大多都可查,这里讲讲新生代的 to 和 from 为啥要这么设计。

    新生代管理短时间占用内存空间的引用,默认情况下,32 位系统新生代内存大小为 16MB,64 位系统下,新生代内存大小为 32MB。

    因为新生代管理的是不稳定的引用,大部分是很快就销毁的引用(只用几次的变量或者阅后即焚类型的)也就是说新生代只需要复制还活着的内存引用,其余的三下五除二全部清空掉。这样的好处就是对于新生代管理的内存而言,大部分都是不稳定的,只有少部分稳定。它只需要关心少部分稳定的即可,其他大部分的一股脑全干了,效率会比较高。

  15. 监控性能数据你是如何选定指标的

    从 performance API 去分析,针对不同阶段大致分成网络阶段,资源下载阶段,DOM 解析阶段三部分。具体细节可以参考 MDN 的文档

    对于路由按需加载的 SPA 应用来说,这样分析也是不准确的。当路由切换的时候再去请求的时候,实际上请求的不是 html 而是 js 文件。这样会让 domContentLoadedEventEnd 监测不到了相关的数据。可以参考谷歌用户体验追踪的文档,有个 FSP 的概念

    除了 FSP 还有更硬核的解决方案,就是利用 Mutation Observer 来监控整个 DOM 树的变化,并且每个节点会给他赋权,比如图片感觉比较重要,那就图片权重为 10,文字权重 5,title 权重 15 等。最后再计算一个值,来得出当前屏幕主要元素绘制完毕时候的时间节点

  16. 其他很多不记得了,字节面完有一段时间了,大概记得的是这些

    有些和鹅厂重复,可以参考鹅厂攻略

    Vue 双向绑定原理,实现简单版的 Vue 包括 Watcher 发布订阅等,service worker 介绍,http2 等

百度面经

面试进度:offer

面试难度:⭐️⭐️

面试风格:常规问题 + 简单算法

过程介绍:

百度是我把简历挂出去之后,hr 联系我,问我要不要面的。是一个 QA 部门。3 轮技术面,1 轮产品面,1 轮 hr 面。面试官都很年轻,有个好像是和我同届(19 年毕业)的。面试过程感觉还不错,就是约面的 hr 感觉有点怪怪的,可能也是实习生或者工作不久,沟通和执行效率都比  较低。但是最后 hr 面试时那个 hrbp 给人感觉就很舒服,和鹅厂的 hr 给人感觉差不多

这次面试百度说实话感觉偏简单,但是会有一轮产品面试。也可能是面试官经验不怎么丰富。一二面的面试题都实在太浅,三面应该是 leader,但是是后端的,会问一些简单的算法。

面试题:

  1. Vue 生命周期介绍

  2. Vue computed 和 methods 有什么区别

  3. Vue $router 和 $route 的区别

  4. Vue 2.x 和 3.x 怎么劫持对象

    Object.defineProperty(),Proxy

  5. SPA 想做 SEO 有什么解决方案

    除了 SSR 和预渲染等方案,面试官还提到 noscript 标签也可以做 SEO

  6. 301 状态码和 302 状态码对 SEO 有什么影响

    301 是永久重定向,搜索引擎会把旧资源的 SEO 权重全部重新分配。302 是临时重定向,只是暂时修改了资源地址,权重还是在之前的资源上

  7. 常见 http 状态码

    说完后追问了下 401 和 403 的区别。区别就是一个还没鉴权,一个是鉴权完没权限

  8. 介绍了解的排序

  9. 说说快排如何实现

    Array.prototype.quickSort = function () {
     if (!this.length) return []
    let leftArr = []
    let rightArr = []
    for (let i = 1; i < this.length; i++) {
    if (this[i] >= this[0]) rightArr.push(this[i])
    if (this[i] < this[0]) leftArr.push(this[i])
    }
    return [...leftArr.quickSort(), this[0], ...rightArr.quickSort()]
    }
  10. 描述一下多层级的分类目录,如何组装数据结构

  11. 描述一下大数相乘

    // 没有让写,电话面试的让我描述一下
    // 大数相加相乘还是比较有意义的,这里我写一下吧
    /**
    * 大数相乘
    * @param {String} a 数字字符
    * @param {String} b 数字字符
    */
    const pow = (a, b) => {
    if (a === '0' || b === '0') return '0'
    // a和b相乘的结果c,c的位数 <= a和b位数的和
    // 初始化一个数组能装下最大的 a * b 的结果
    let res = new Array(a.length + b.length).fill(0)
    // 模拟末尾开始相乘
    for (let i = a.length - 1; i >= 0; i--) {
    for (let j = b.length - 1; j >= 0; j--) {
    const sum = parseInt(a[i]) * parseInt(b[j]) + res[i + j + 1]
    res[i + j + 1] = sum % 10
    res[i + j] += Math.floor(sum / 10)
    }
    }
    // 注意初始化的数组,第一位有可能是用不上的
    return res[0] === 0 ? res.slice(1, res.length).join('') : res.join('')
    }
  12. 其他不太记得了,总体来说比较基础

    没有问原理和源码相关的东西,算法也不难,不需要手写,讲思路即可

富途面经

面试进度:3 面,不太喜欢就拒了后面的邀约

面试难度:⭐️⭐️⭐️

面试风格:少量项目 + 部分算法 + 部分逻辑题

过程介绍:

富途是猎头还是什么平台帮我自动投递的,是一个做内部支撑的部门。3 轮技术面,1 轮 hr 面。我不知道是不是因为猎头推的原因,约面非常敷衍,联系方式也不留,也就一个座机号,没接到电话打回去也找不到人。然后隔了一周多,才第二次联系。而且必须是现场面试,必须是工作日,必须是白天。一面面试官还是挺友好的,但二面面试官有点高冷和不耐烦。

面试风格比较独特,喜欢考逻辑题和智力题,也会问一些应用题和场景设计。一面前要做个笔试,网上的题目,原题一摸一样那种。如果某题有多种解法,面试官会不断问新的解题思路以及一直引导你,直到答到他自己想听的那个答案。整个过程很慢,基本上一个半小时以上。

面试题:

  1. CSS 实现左右布局,左边定宽,右边自适应

    flexbox

  2. 斐波那契数列求第 n 个数,要做优化版的

    // 动态规划,对象缓存,闭包缓存都行,这里写个动态规划吧
    /**
    * 动态规划斐波那契,其实和 leetcode 70题爬楼梯是一样的
    * @param {Number} n 要求的数
    */
    const fib = function (n) {
    let dp = [1, 1]
    for (let i = 2; i < n + 1; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]
    }
    return dp[n]
    }
  3. 括号匹配

    // leetcode 20题,用栈,比较简单
    /**
    * 括号匹配
    * @param {String} s 输入的字符串
    */
    const isValid = (s) => {
    let map = {
    '(': 1,
    ')': -1,
    '{': 2,
    '}': -2,
    '[': 3,
    ']': -3,
    }
    let stack = []
    for (let i in s) {
    if (map[s[i]] > 0) {
    stack.push(s[i])
    } else {
    const target = stack.pop()
    if (map[target] + map[s[i]] !== 0) return false
    }
    }
    if (stack.length > 0) return false
    return true
    }
  4. 加载页面有哪些性能优化的方案

    这问题和 “输入 url 发生了什么” 这问题类似,简单介绍 5 分钟能讲完,要扯皮的讲半天也可以讲...

    常规的:图片懒加载,预加载,雪碧图,路由按需加载,gzip ......

    网络的:http2,强缓存,协商缓存,合并请求资源(http2 就不用合了)......

    其他:service worker ......

  5. 一个无序数组,找出比他左边都大,比他右边都小的元素,要求时间复杂度 On

    没让我写,让我说说思路

    时间复杂度 On 的话,不嵌套循环就好了

    1. 第一遍遍历从头开始,用一个新数组 max 存每一个遍历元素它左边的最大值
    2. 第二遍遍历从尾开始,用一个新数组 min 存每一个遍历元素它右边的最小值
    3. 第三遍遍历,当前元素和 max,min 两个数组对应下标的值比较,找到满足条件的

    例:当前元素下标 3,值是 5。max[3] 是 4,min[3]是 8,那当前元素就符合条件( 5 > 4 && 5 < 8 )

    这样时间复杂度是 3 * On,舍弃常数也就是 On

  6. 逻辑题,称重

    10 瓶药,每瓶 100 片,其中 9 瓶是 10g/片,1 瓶 9g/片,问只称重一次,怎么找出轻的那瓶药

    讲道理这题我想了好一会...最不喜欢面试做这类题

    第一瓶拿 1 片,第二瓶拿 2 片,以此类推

    如果都是 10g,那么一共是 (1 + ... + 10) * 10 = 55 克。但是实际上会少几克,因为有一瓶每片药是 9g 的。如果称的是 53g,那么意味着有 2 片药是 9g 的,那就是第二瓶药是有问题的(因为第二瓶药我们拿出了两片)同理如果 52g,那就是 3 片药有问题,那瓶药是第三瓶药

    面试官后面还追加了一问,如果是两瓶有问题的,又该如何称重,上面的方法可行吗?答案是不可行,原因留给大家想吧,tips 就是上面是等差取药片,需要改成等比取

  7. 设计一个抽奖程序

    开放题没啥好说的,就问了一下接口如何设计,然后有什么注意的事项。还有描述抽奖函数的实现

  8. 其他

    这两题网上是有原题的,面完还搜了一下。然后还有些不记得了

    1. 读 C++ 代码写结果,计算个人所得税的函数

    2. Room 和 User 两个类,现在有个关门的方法,放到哪个类中

转转面经

面试进度:offer

面试难度:⭐️⭐️

面试风格:常规问题 + 一点算法

过程介绍:

转转面的深圳这边的团队,3 轮技术面,1 轮 hr 面。面试效率很高,基本上一天一面,一周完事。

问的大多是常规问题,问的不深,但是涉及的基础面比较广。面试是北京那边的前端团队远程视频面。会涉及到一些计算机网络,数据结构等计算机基础,但是问的都比较浅。印象比较深刻的是 3 面的前端 leader,思维很广,看问题的角度明显就不太一样。后来了解到之前好像是百度文库的负责人。整个转转很多是百度出来的人。

面试题:

  1. Vue 双向绑定原理

    基本是必问的,Vue 相关问题参考鹅厂面经吧,鹅厂挖的很深了

  2. Vue 生命周期

  3. Vue nexttick 原理

    Vue 双向绑定的 Watcher 源码中有这么一句 queueWatcher(this),而这玩意内又有这么一句 nextTick(flushSchedulerQueue)。这个 flushSchedulerQueue 其实就是更新视图的函数。

    抽象概括一下 nextTick 的原理:

    1. nextTick 接收一个回调,返回一个闭包

    2. 回调的执行时机 nextTick 闭包中的 timerFunc 控制

      timerFunc 用以下优先级来处理出队时机
      setImmediate -> MessageChannel -> Promise -> setTimeout
      优先使用宏任务,如果不支持就使用 Promise 的微任务,如果还不支持就用 setTimeout 兼容。虽然setTimeout 是宏任务,但是在 http 声明中 setTimeout 有 4ms 的延时,所以只拿来做兼容处理,宏任务交给 setImmediate 和 MessageChannel

    也就是说用于更新视图的 nextTick(flushSchedulerQueue) 其实以 setImmediate -> MessageChannel -> Promise -> setTimeout 优先级塞入了 Event loop 中

    这样更新的好处是:

    假设有个值 test 被 while 循环执行 1000 次++操作。每次循环时,都会根据响应式触发 compile -> setter -> Dep -> Watcher -> update -> run 如果没有异步更新,而是每次都更新 DOM,那会十分消耗性能。所以 Vue 实现了一个 queue 队列,先把所有的 Watcher 塞到队列里。在下一个 tick(或者是当前 tick 的微任务阶段)的时候会统一执行 queue 中 Watcher 的 run(),也就是这 1000 次++操作,其实 Vue 只处理了 0 -> 1000 这一次操作

    总结:

    第一阶段:queueWatcher(this) 收集 watcher 队列

    1. 响应式触发 update,把 watcher 塞到 queue 队列,并且根据 watcher.id 去重

    第二阶段:nextTick(flushSchedulerQueue) 更新视图

    1. nextTick 会返回一个闭包,通过 timerFunc 执行 flushSchedulerQueue 回调
    2. flushSchedulerQueue 执行 watcher 的 run,更新视图
  4. 基础数据类型和引用数据类型差别

  5. 堆和栈的差别

  6. 深拷贝和浅拷贝有什么不同,怎么实现深拷贝

    深拷贝就是递归的浅拷贝

  7. 什么是 options 请求

    参考字节面经 No.11

  8. H5 适配原理,px2rem 是怎么适配的

    rem 就是 html 标签上 font-zise 的值,只要让这个值跟随屏幕自适应就好了。那自然是和 vw 挂钩啦。然后还有配置 meta 标签,还有换算系数和几个视窗概念等,感兴趣的可以查一下,这里不废话了

  9. 什么是 BFC

    MDN 走起

  10. commonjs 和 esmodule 有什么区别

    1. CommonJS 是被加载的时候运行,ES6 模块化是编译的时候确定依赖,也就是说重复引入同一个模块,也只会执行一次代码。tree sharking 原理就是 ES6 模块依赖是编译时确定的。
    2. CommonJS 输出的是值的浅拷贝,并且写到内存中。ES6 模块化输出值的引用
    3. CommonJS 支持动态导入,也就是 require(${path}/xx.js)
    4. CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  11. webpack 构建后的输出,是基于什么模块协议的

    目前浏览器端 js 模块化大概两类,一类是 CMD 就近依赖,一类是 AMD 提前依赖

    所谓就近依赖就是,用到的时候再声明加载依赖。提前依赖就是事先声明好依赖关系

    /** AMD写法 **/
    define(['a', 'b', 'c', 'd', 'e', 'f'], function (a, b, c, d, e, f) {
    // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething()
    if (false) {
    // 即便没用到某个模块 b,但 b 还是提前执行了
    b.doSomething()
    }
    })
    /** CMD写法 **/
    define(function (require, exports, module) {
    var a = require('./a') //在需要时申明
    a.doSomething()
    if (false) {
    var b = require('./b')
    b.doSomething()
    }
    })

    而 webpack 打包后会变成 key,value 的形式的键值对,key 是路径,value 是模块的闭包函数。没仔细看构建源码,猜测是基于 CMD 协议的,也就是就近依赖。因为这才比较符合按需加载的逻辑

  12. 介绍浏览器事件循环和 node 事件循环

    关于事件循环,作为前端开发者肯定都了解过。但是真的往深了去问又会发现很多人其实理解的模模糊糊,不太确定的样子。我简单说说我的理解。

    浏览器端:

    浏览器端事件循环两个关键就是宏任务和微任务两个老哥。每一轮事件循环只会在宏任务队列里出队一个宏任务并且执行,然后清空整个微任务队列。都完事后就结束这轮的事件循环,再搞一个宏任务出来并且清空微任务队列,循环往复。

    常见的宏任务有,同步的代码,setTimeout,setInterval,requestAnimationFrame,以及 node 中的 setImmediate 等
    常见的微任务有,promise,await async 等。node 中有个 process.nextTick 比较特殊,后面说
    

    node 端:

    node 端每一轮循环都有下面几个阶段

    1. timers ,首先是执行定时器,如果在当前循环种存在 setTimeout / setInterval 之类的定时器,将在这时候处理

    2. pending callbacks,这个阶段也称为 I/O callbacks 阶段,主要是执行一些从上个循环延迟过来的 I/O 回调

    3. idle,prepare,node 内部机制

    4. poll,轮询,在这里检索新的 I/O 事件;执行与 I/O 相关的回调,其余情况 node 将在适当的时候在此阻塞,直到达到最快的一个计时器阈值为止(比如 setTimeout 100ms,那如果这时候没事做,那他就等这个 setTimeout 100ms)

    5. check,执行 setImmediate 的回调

    6. close callbacks,关闭当前循环

    另外还有个 process.nextTick 这玩意,它并不属于宏任务也不属于微任务,他是进入当前循环阶段的时候,最优先执行的那个。并且 node 循环的每个阶段这老哥都能执行。建议网上找些实际的例子看看,光看描述其实理解起来比较抽象

    最后补充:

    其实 js 的异步并不是它自己玩的。拿浏览器来说,一个 tab 渲染进程包含了好几个线程:

    1. 渲染线程,与 js 线程互斥
    2. js 线程,与渲染线程互斥
    3. 事件处理线程,收集异步回调,触发时塞到 js 线程的处理队列
    4. 定时器线程,类似事件处理线程,触发时将定时器塞到 js 线程的处理队列
    5. 异步 http 线程,当 ajax 状态发生改变时,就把回调塞到 js 线程的处理队列

    也就是说所谓异步事件,setTImeout 也好 promise 也好,其实不是 js 的主线程自己搞的,他只是遇到这些操作的时候交给对应的异步处理线程,他们搞定了再回过头推进宏任务 / 微任务队列。而 js 线程的主要工作只是不断事件循环,检查任务队列,有东西执行他就执行。node 中也是类似的,只不过服务端可调度的线程资源池就交给了 C++ 大哥的 libuv 库。有时间我再写一篇关于 js 主线相关的东西,讲讲浏览器那几个线程以及异步怎么处理,还有 node 关于线程池的分配等。了解一些底层的原理,被问到 js 单线程之类的就不虚了

  13. 什么是强缓存,什么是协商缓存

    缓存也是被问到烂的问题了,所谓强缓存就是本地缓存,状态码是 200。协商缓存就是到服务器协商过后看要不要用缓存结果,状态码是 304。

    强缓存有 exprie 和 cache-control 两种缓存形式
    前者是绝对时间缓存,受客户端时间影响;后者是相对时间
    协商缓存有 If-None-Match 和 If-Modified-Since 两种形式
    前者是 Etag 判断资源 hash 是否变更,后者是时间维度的对比
    细节可以查查资料,这里不过多介绍

    顺便补充一下常见缓存的优先级

    1. 强缓存
    2. sw 缓存的 App Cache
    3. 协商缓存
    4. http2 的 server push
  14. async / await 和 promise 有什么区别

    是不是想答 async / await 是同步的写法实现异步的能力,代码更简洁易读

    那要是面试官追问实现方式甚至问性能优势呢?

    关于 async / await 我再补充点吧

    首先是总所周知系列:

    1. async / await 是同步的写法

    2. try...catch... 捕获异常

    3. 是 generator 的语法糖

    关于性能,我主要想说说第三点。

    关于 generator 和 iterator 的关系我就不多说了,我主要说说这玩意是怎么实现暂停代码的效果的。首先要明确一点就是 generator 本质上也是一个函数,当他执行的时候,一样会被 js 引擎推到函数执行栈中。但是他和正常函数有一个不同:

    正常函数执行完,就从栈顶弹出然后就销毁。但是 generator 不一样,这玩意执行完之后返回了一个 iterator 迭代器,这个迭代器保存着 generator 的引用

    这是关键,大家想想闭包和内存泄漏,不就是因为某个地方还保存着变量的引用么?是的,这迭代器的存在导致这个生成器函数并不会被销毁,哪怕它被执行栈弹出了。当再执行迭代器 iterator 的时候,又回通过指针找到这个生成器 generator,把它推进栈顶并且执行直到遇到 yield,然后再弹出生成器,并且返回一个包含 value 和 next 指针的对象。

    言归正传,async / await 既然是语法糖,那它背后原理就如上所述。那和性能有毛线关系呢?大家再想想 promise,promise 是需要在 then 的时候收集依赖并且 push 到 _resolveQueue 和 _rejectQueue 两个队列中存起来的,然后当 resolve 或者 reject 的时候去遍历对应的执行队列,执行回调。也就是说 promise 是需要保存和维护函数执行依赖的,对于 promise 链式调用来说,所有的 then 依赖关系都要保存。而基于迭代器的 async / await 就不需要保存一堆依赖

  15. for in 和 for of 有什么区别,可以 for of 对象吗

    1. foreach 是遍历数组的,但是它不可中断,return 不了

    2. for in 是遍历对象的,但是也可以遍历数组(毕竟也是 object)。但是 for in 遍历数组的时候 index 会当成 key 来处理,此时 index 是 string 类型而不是 number 类型。另外会把数组的属性给遍历出来。比如 arr.name

    3. for of 是遍历可迭代对象的,比如数组,迭代器等。它没有下标,数组的话直接把元素遍历出来,但是不包括数组上的方法。

    4. 一般对象并没有可迭代的属性 iterator,正常情况下是不能 for of 对象的。不过可以手动给对象绑上迭代属性,就可以 for of 对象了

    const iteratorObj = (obj) => {
      obj[Symbol.iterator] = function* () {
    // 关键在于需要返回一个迭代器,就是 yield 返回的那个中间对象 { done, value }
    // generator 就是拿来生成迭代器的,所以直接用 genFn
    let keys = Object.keys(this)
    for (let i = 0; i < keys.length; i++) {
    yield this[keys[i]]
    }
    }
    }
  16. webpack 的 loader 和 plugin 有什么区别

    loader 可以理解成翻译器,遇到不同类型的文件,翻译成 js 能理解的语言。比如遇到 css 就用 css-loader 翻译器,比如遇到图片就用 file-loader 翻译器等。

    plugin 是在 webpack 不同生命周期中做一些特定的事情,来扩充 webpack 能力的。比如构建之前清空 output 目录,比如构建完成后移动文件等。

  17. 无重复最长子串

    // leetcode 3题
    // 思路是用 map 做移动窗口的匹配
    /**
    * 找出无重复字符的最长子串
    * @param {string} s 字符串
    */
    const lengthOfLongestSubstring = (s) => {
    let map = new Map()
    let startIndex = -1
    let max = 0
    for (let i in s) {
    if (map.has(s[i])) startIndex = Math.max(map.get(s[i]), startIndex)
    max = Math.max(max, i - startIndex)
    map.set(s[i], i)
    }
    return max
    }
  18. 括号匹配

    参考富途面经 No.3

总结

复习策略:

  1. 系统复习前端基础。如果你的方向不侧重于样式重构,html 和 css 大致了解有个概念即可。至于 js 则需要重点复习。另外平时也建议找一本权威的书好好啃一下,比如红皮书,js 精粹,js 忍者秘籍,函数式编程等等,有空读读书也是挺好的。

    事实上工作过程中 html 和 css 的积累已经足够应付面试了。而 js 因为是整个前端编译语言的基础,加之这玩意诞生之初到发展到今天,作为一个弱类型的语言其实有很多性能上或者奇奇怪怪的问题,这些问题如果不深入系统复习,其实是很难去理解和解释它的。比如 this 是啥,箭头函数是啥,数组又有哪些高阶用法,异步又是什么原理,闭包又是个啥玩意等等...

  2. 深入了解某一个前端框架的源码。现在前端面试者这么多,在业务层和应用层多多少少都会用一些框架。那大厂要筛更好的人怎么筛?那就只能深挖一些原理去考核。所以大厂面试源码基本是必问的。但是这些东西一般是触类旁通,无非是要熟知框架的设计理念,核心方法,表层 API 背后的原理等。

    如果觉得啃源码太累,也能找找别人整理好的资料,甚至买些源码解读的视频也可以

  3. 算法。如果你看完了这些面经,我想你也发现了基本所有厂都有问算法。这次面试下来各种各样的面试风格都遇到过,但无论哪一种风格的面试都好,算法可以说是必考的。算法的复习是急不来的,肯定是日积月累的。我的建议是每天有空刷一下题,给自己定一个目标。

    如果要学习算法。最好是积累性质的复习,可以找找大牛总结的常见的算法类型或者买本书,先归类再系统学习。比如针对数据结构的算法,或者针对递归的,针对动态规划的等等。但是如果是要突击复习的话,最有效的方法还是刷 leetcode,因为基本上所有算法面试题都是上面找的。

  4. 牢记计算机基础。计算机网络,数据结构的基础知识我个人认为是必须熟练掌握的。其中重中之重我认为是计算机网络。作为一个前端,前后端交互是绝对少不了的。那怎么交互?大部分情况还是网络上的交互。所以整个网络模型,从应用层的 http1,2,3,websocket 开始,到传输和应用夹层的 TLS 安全,到传输层的 TCP 和 UDP,都是很重要的知识点。数据结构的数组,队列,栈,链表,树,字典,图等也是必须掌握。

    操作系统看个人方向,前端涉及到的不多,如果想做底层架构或者 IOT 之类路线的可以重视一下

  5. 找面经和真题。针对面试而言,临时抱佛脚其实还是很有用的。因为在同一时间段内大部分公司其实面试题都八九不离十。遇到原题或者同类型题的概率还是很大的。但是大厂就不一定了,大厂部门多,面试官多,题库资源雄厚,想靠背真题去通关几乎是不现实的。

个人规划:

职业规划其实因人而异了,但是在个人能力提升上我是这么看的:无论做什么事情,没有目标,没有合理的规划,其实是很难成长的。有目标才能督促自己去做点什么。那从技术规划上,可以考虑全栈的发展,或者专注前端领域跨平台的发展,亦或者对 SDK 或框架设计能力的提升等等。

需要有扩展性视野。也就是多了解上下游的技术栈以及前沿技术信息。平时多看多关注优质社区文章和开源项目,去慢慢积累与巩固自己的知识面。

技术深度上比如大前端,node,v8,ts,或者某个感兴趣的领域的深入研究。

技术广度上比如 CI / CD,Docker,Flutter / Electron 等混合开发,微前端,serverless 的应用场景等。

从软实力上更多是培养自己的一些好习惯,比如习惯性的读书,周期性的发表文章,甚至极客一点的搞搞乱七八糟的服务器部署些有实质意义的应用,或者开源一些个人框架等等。

最后就是一定要保持学习的态度。

By @Evan
2020.09.18

« 应届前端的逆袭(下)

大前端相关面试题 »

Evan的博客

Evan 的博客 - 非典型码农,bug永动机
Instagram Weibo GitHub Email RSS

Categories

面试 原理笔记 项目实践 其他 JS Vue 性能优化 算法 计算机网络

Recent Posts

  • 从 HTTP 发展历程重学计算机网络
  • 应届前端的逆袭(中)
  • 应届前端的逆袭(上)
  • 应届前端的逆袭(下)
  • 前端面经复盘

Copyright © 2015 Powered by MWeb,  Theme used GitHub CSS.