新鲜出炉滴滴,阿里,字节,快手前端面经(2021.1)

2,129 阅读34分钟

先介绍一下本人的情况,本人双非本,2年+工作经验,但是第一份工作基本使用的是JSP,前后端都写,所以是自学的vue,自己也跟着写过vue demo项目,但直到第二份工作,才有了正式上线前端项目的经验,可以说真正的前端开发经验是1年左右,原本是打算在第二份工作上好好积累的,不过因为公司运转出现了问题,所以不得不又在比较短的时间开始了我这次找工作之旅。

滴滴

一面

面试流程:

  • 自我介绍
  • 简历项目深挖
  • 基础知识
  • 算法
  • 职业规划
  • 提问环节

面试题目:

vue中的data支持什么类型?什么时候使用Function?为什么?

vue data选项支持Object和Function两种类型,在组件里,必须使用Function,官方文档中是这样解释的:

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

vue插件是怎么实现的?

这个和我简历上的项目有关。主要是实现了install方法。

讲讲简历上项目的量级,有什么特点难点?

谈谈对vuex的理解,为什么要区分mutation和action?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。状态保存在state中,通过mutation同步更新状态,action中是异步对状态的更新。

实际上 vuex 是学 redux 的, redux 说把异步放在 action creator 里(对应 vuex 的 action),使得 reducer 是纯函数 (对应 vuex 的 mutation),这样最起码 reducer/mutation 是可以保证正确性的。

纯函数既是统一输入就会统一输出,没有任何副作用;如果是异步则会引入额外的副作用,导致更改后的state不可预测。

vue-router结合webpack怎么做懒加载?

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。

webpack 提供了两个类似的技术用于动态拆分代码。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。

所以最终我们想要实现的vue-router懒加载如下:

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')

讲讲xss、csrf、sql攻击及解决方案?

这个问题我推荐大家去看看我之前写过的博客

XSS、CSRF攻击问题详解

讲讲什么是正向代理和反向代理?

反向代理(reverse proxy):

  • 定义:是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
  • 用途:
    • 隐藏服务器真实IP
    • 负载均衡
    • 提高访问速度
    • 提供安全保障

正向代理(forward proxy):

  • 定义:是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端。
  • 用途:
    • 突破访问限制
    • 提高访问速度
    • 隐藏客户端真实IP

两者区别:

  • 正向代理其实是客户端的代理,反向代理则是服务器的代理
  • 正向代理一般是客户端架设的,反向代理一般是服务器架设的
  • 正向代理中,服务器不知道真正的客户端到底是谁,反向代理中,客户端不知道真正的服务器是谁
  • 正向代理主要是用来解决访问限制问题。而反向代理则是提供负载均衡、安全防护等作用

http 2.0和http 1.0的区别?

HTTP1.0 HTTP 1.1 HTTP 2.0主要区别

HTTP各版本特性及区别

JS单线程会有哪些问题?

这里面试官就是想引出JS事件机制。还是推荐一下我之前写的文章。

JavaScript执行机制

Promise的实现原理?

建议大家好好看下Promise/A+规范,跟着规范写下Promise。

面试官:“你能手写一个 Promise 吗”

前端有哪些性能优化?

content方面

  1. 减少http请求:合并文件,css精灵,图片
  2. 减少DNS查询,DNS缓存,将资源分布到恰量的主机名
  3. 减少dom操作 server端
  4. 使用cdn
  5. 配置Etag
  6. 对组件使用Gzip压缩 cookie方面
  7. 减少cookie的大小 css方面
  8. 将样式表放到页面顶部
  9. 不使用css表达式
  10. 使用 不使用@important JavaScript方面
  11. 将脚本放到页面底部
  12. js和css外部引入
  13. 压缩js和css代码
  14. 减少dom的访问 图片方面
  15. 优化图片
  16. 优化css精灵
  17. 不要在html中拉伸图片

window.performance API都包含哪些时间节点?

常用性能指标计算:

  • DNS查询耗时 = domainLookupEnd - domainLookupStart
  • TCP链接耗时 = connectEnd - connectStart
  • request请求耗时 = responseEnd - responseStart
  • 解析dom树耗时 = domComplete - domInteractive
  • 白屏时间 = domloadng - fetchStart
  • domready时间 = domContentLoadedEventEnd - fetchStart
  • onload时间 = loadEventEnd - fetchStart

你工作中的git flow?

我们公司因为规模比较小,所以git flow也比较简单,这里面试官推荐我去了解下 Husky,在大规模开发的时候,用来做提交前的ESLint检测比较有用。

算法:求1加到100,有几种解题思路,他们各自的时间复杂度如何?

这里的面试都没有手撕算法,就是讲下思路。这个求和有两种思路:

  1. 暴力循环求解,没啥好说的,时间复杂度O(n)
  2. 利用等差数列求和公式,时间复杂度O(1)

算法:讲讲斐波那契数列的实现?

斐波那契就是一个典型的递归问题,单纯的递归,他的复杂度取决于递归的深度,当n的值很大时,递归将特别耗时,所以有两种优化方案:

  1. 将递归问题简化成一个动态规划问题,通过O(n)的时间复杂度求解
  2. 用一个状态管理,把每次获得的值保存起来,当下次重复用到的时候可以减少计算

借着算法随便八卦了下,你觉得前端为什么要学算法?聊聊你的职业规划?

这里就讲了点我自己的看法,顺便讲了下前端中会用到的算法,并用vue源码中的diff算法举了个例子。

小结

面试官人还是挺好的,面试的过程也比较舒服,虽然我自我感觉答的都还是不错的,但JD明显感觉到要招的的年限和我工作经验不相符,面完面试官隐晦的说可以删了他好友时就知道这趟滴滴之旅结束了。

阿里盒马

笔试 + 电话初探

面试流程

  • 笔试(1小时线上完成)
  • 电面
    • 自我介绍
    • 项目深挖
    • 基础知识
    • 职业规划
    • 提问环节

面试题目

笔试一:实现本地的add方法,调用addRemote,能最优的实现输入数字的加法。

感觉这道题是个动归,但因为每次加法逻辑是要请求服务端接口,所以可能还涉及到http通信的问题,能够合并或者并发请求应该会更好。

笔试二:实现一个html语法树转化html的工具类,并添加一个方法用于移动节点。

这道题其实考的知识点还是挺全的,涉及到ast树转译,和diff算法的实现。

介绍项目中的重点亮点。

开门都是问项目的重难点,所以大家真的要好好总结总结自己做过的项目。

口述实现一个深拷贝

这里直接上代码吧,虽然是口述,但实际上面试官问的很细致,几乎每一步都一字不差的让你说了出来。

function clone(target, map = new Map()) {
  if (typeof target === 'object') {
    // 区分数组和对象
    let cloneTarget = Array.isArray(target) ? [] : {};
    // 处理循环引用
    if (map.get(target)) {
      return map.get(target);
    }
    for (key in target) {
      cloneTarget[key] = clone(target[key], map);
    }
    map.set(target, cloneTarget);
    return cloneTarget;
  } else {
    return target;
  }
}

用webpack可以做哪些性能优化?

  • OptimizeCssAssetsWebpackPlugin:压缩css
  • babel缓存
  • 使用ES6 modules可以实现tree-shaking
  • splitChunks:抽取入口文件的公共方法到单独的chunk
  • 多进程打包
  • externals:拒绝打包,可以使用外部CDN
  • DllReferencePlugin:第三方的库不参与打包

谈谈对npm语义版本号的理解?

npm语义版本号控制

谈谈你对Promise的理解?

Promise 是一种解决一步编程的方案,比其他传统方案(比如回调函数和事件)更合理和强大。它是位解决一步处理回调地狱问题而产生的。

又是Promise的问题,还是看上面提到的那篇文章。

Promise.then方法中的reject回调和catch中的回调有什么区别?

Promise中的then第二个参数和catch有什么区别

小结

阿里的面试问的都会比较深入,针对一个知识点会不停向下挖掘,所以再次感受到了一知半解的局限,面试的过程也是检查自己的不足,这里也给大家一个警示吧,面试官其实挺好的,遇到你不会的问题会引导你,还有就是面试的时候心态要好一点,有时候面试官不仅仅考察你对知识点的掌握程度,也会观察你如何处理遇到的问题。

字节抖音

一面

面试流程

  • 自我介绍
  • 项目深挖
  • 基础知识
  • 编程题
  • 提问环节

面试题目

了解哪些设计模式?

JavaScript中的设计模式

了解过vue的composition api吗?

这个就是vue3.0的升级,建议大家看看官方文档。

Vue 组合式 API

讲讲vue keep-alive?

  • keep-alive是什么?
    • keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • 作用
    • keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。 总的来说,keep-alive用于保存组件的渲染状态。
  • 属性
    • include:缓存白名单
    • exclude:缓存黑名单
    • max:缓存的组件实例数量上限
  • 使用注意点
    • 被keep-alive包裹的组件,不会再触发mounted和mounted以前的事件。
    • 当组件在keep-alive内被切换,它的activated和deactivated这两个生命周期钩子函数将会被对应执行。
    • keep-alive要求同时只有一个子元素被渲染。

ES6 modules有哪些注意点,和commonjs的区别?

注意点:

  1. export命令导出必须以接口(变量)的方式,不能导出值的方式;
  2. export,import命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷;
  3. import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同;
  4. import命令输入的变量都是只读的,因为它的本质是输入接口;
  5. import命令具有提升效果,会提升到整个模块的头部,首先执行;
  6. 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构;
  7. 使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出;
  8. 正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后;
  9. import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

区别:

  • CommonJS 模块加载过程是同步阻塞性地加载,在模块代码被运行前就已经写入了 cache,同一个模块被多次 require 时只会执行一次,重复的 require 得到的是相同的 exports 引用。
  • ES6 模块会在程序开始前先根据模块关系查找到所有模块,生成一个无环关系图,并将所有模块实例都创建好,这种方式天然地避免了循环引用的问题,当然也有模块加载缓存,重复 import 同一个模块,只会执行一次代码。
  • CommonJS 可以在运行时使用变量进行 require, 例如 require(path.join('xxxx', 'xxx.js')),而静态 import 语法(还有动态 import,返回 Promise)不行,因为 ES6 模块会先解析所有模块再执行代码。
  • require 会将完整的 exports 对象引入,import 可以只 import 部分必要的内容,这也是为什么使用 Tree Shaking 时必须使用 ES6 模块 的写法。
  • import 另一个模块没有 export 的变量,在代码执行前就会报错,而 CommonJS 是在模块运行时才报错。

webpack打包过程,及最终产物?

打包流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始
  3. 执行编译;确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 webpack

最终产物:

  • 根据入口文件和依赖关系生成的多个chunk
  • 对代码的兼容处理产物

webpack中使用import的场景和作用?

  • 路由懒加载
  • tree-shaking

谈谈你对响应式布局的理解?

  1. flexbox
  2. 百分比
  3. calc
  4. @media 除了高度宽度还能判断啥:媒体类型,媒体功能
  5. meta initial scale mobile device
  6. rem:控制不同设备下的根font-size,需结合媒体查询

谈谈你对canvas的理解?

  • ECharts绘制的图表
  • 动画,游戏
  • 自定义海报

canvas 2d影响性能的点在哪里?

影响的点:

  • 为了动画的流畅,一帧的时间只有16ms。在这 16ms 中,不仅需要处理游戏逻辑,计算每个对象的位置、状态,还需要把它们都画出来。如果消耗时增加,用户就会感受到「卡顿」。
  • 对某个 API 的调用过于频繁,导致渲染的耗时延长

优化方法:

  • 在离屏canvas上预渲染相似的图形或重复的对象
  • 避免浮点数的坐标点,用整数取而代之:画一个没有整数坐标点的对象时会发生子像素渲染。
  • 不要在用drawImage时缩放图像:在离屏canvas中缓存图片的不同尺寸。
  • 使用多层画布去画一个复杂的场景
  • 用CSS设置大的背景图
  • 用CSS transforms特性缩放画布
  • 关闭透明度

编程题1:实现Promise.all方法

编程题2:

Array.findDuplicate(n) 用于返回该数组中出现频率>=n的元素列表
 ['1',2,'3','4',2,2,'2',2,'3','3'].findDuplicate(3)
>  ['3', 2] // 或者 [2, '3']

解答:

Array.prototype.findDuplicate = function (n) {
  let countSet = new Map();
  let res = [];
  for (let i = 0; i < this.length; i++) {
    if (countSet.get(this[i])) {
      countSet.set(this[i], countSet.get(this[i])+1);
    } else {
      countSet.set(this[i], 1);
    }
    if (countSet.get(this[i]) >= n) {
      if (res.indexOf(this[i]) === -1) {
        res.push(this[i]);
      }
    }
  }
  return res;
}

二面

面试流程

  • 基础知识
  • 编程题
  • 提问环节

面试题目

堆和栈的区别?

 栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。在编程中,例如C/C++中,所有的局部变量都是从栈中分配内存空间,实际上也不是什么分配,只是从栈顶向上用就行,在退出函数的时候,只是修改栈指针就可以把栈中的内容销毁,所以速度最快。      堆(Heap)是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁时都要占用时间,所以用堆的效率低的多!但是堆的好处是可以做的很大,C/C++对分配的Heap是不初始化的。

数据结构:

  • 堆:堆的数据机构其实就是一个完全二叉树,具堆属性的数据结构才可被叫做为堆,堆常见的应用就是堆排序与实现优先队列,因为堆的存取是随意。
  • 队列:是一种采用先进先出(FIFO)策略的抽象数据结构,它的想法来自于生活中排队的策略。顾客在付款结账的时候,按照到来的先后顺序排队结账,先来的顾客先结账,后来的顾客后结账。一般与栈作比较 。 不过同栈的实现一样,队列的实现也有数组实现和链表实现两种方式。
  • 栈:与队列相反,栈的顺序是后进先出,只可以在栈顶进行操作,类似与只有一个出入口的公交车,先上车的只能后来下车 。

分配方式:

  • 栈是向低地址扩展的数据结构,是一块连续的内存区域
  • 堆是向高地址扩展的数据结构,是不连续的内存区域

JS的数据类型

  • 基础类型:null,undefined,Boolean,Number,String,Symbol
  • 引用类型:Object

Symbol类型是什么样的,有什么用途?

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

Symbol 为 JavaScrit 对象提供私有属性还有点困难,但 Symbol 还有别外一个好处,就是避免当不同的库向对象添加属性存在命名冲突的风险。

HTTP和HTTPS的区别?

详细解析 HTTP 与 HTTPS 的区别

TCP和UDP的区别

  1. 基于连接与无连接;
  2. 对系统资源的要求(TCP较多,UDP少)
  3. UDP程序结构较简单
  4. 流模式与数据报模式
  5. TCP保证数据正确性,UDP可能丢包
  6. TCP保证数据顺序,UDP不保证。

七层网络模型还有哪些层,各有些什么协议?

OSI七层网络模型TCP/IP四层概念模型对应网络协议
应用层(Application)应用层HTTP、TFTP, FTP, NFS, WAIS、SMTP
表示层(Presentation)应用层Telnet, Rlogin, SNMP, Gopher
会话层(Session)应用层SMTP, DNS
传输层(Transport)传输层TCP, UDP
网络层(Network)网络层IP, ICMP, ARP, RARP, AKP, UUCP
数据链路层(Data Link)数据链路层FDDI, Ethernet, Arpanet, PDN, SLIP, PPP
物理层(Physical)数据链路层IEEE 802.1A, IEEE 802.2到IEEE 802.11

websocket有了解吗?怎么使用?

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

使用:

  • onopen:用于指定连接成功后的回调函数。
  • onclose:用于指定连接关闭后的回调函数。
  • onmessage:用于指定收到服务器数据后的回调函数。
  • send:用于向服务器发送数据。

csrf是什么?怎么处理?

XSS、CSRF攻击问题详解

进程和线程的区别?

  • 进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
  • 线程:是进程的一个执行单元,是进程内科调度实体。 比进程更小的独立运行的基本单位。 线程也被称为轻量级进程。

JS在线程上有什么问题?

还是JS事件执行机制。

JavaScript执行机制

JS有哪些处理异步的方法

  • ajax
  • Promise
  • Generator
  • async / await

Generator和async / await使用的区别?

  • Generator 函数是将函数分步骤阻塞 ,只有主动调用 next() 才能进行下一步。
  • async是Generator函数的语法糖,async对Generator函数做了以下4点改变:
    • 内置执行器: async函数自带执行器,使用方法为 asyncReadFile()
    • 更好的语义: async表示函数里有异步操作,await表示紧跟再后面的表达式需要等待结果
    • 更广的适用性: yield命令后,只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值
    • 返回值是Promise对象,可以使用then方法指定下一步操作

快排和归并排序有什么区别?

归并排序:

  • 核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,然后分别对前后两部分进行排序,再将排好序的两部分数据合并在一起。
  • 归并排序使用的是分治思想,分治也即是分而治之,将一个大问题分解为小的子问题来解决。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。

快速排序:

  • 核心思想:如果要对数组区间 [p, r] 的数据进行排序,我们先选择其中任意一个数据作为 pivot(分支点),一般为区间最后一个元素。然后遍历数组,将小于 pivot 的数据放到左边,将大于 pivot 的数据放到右边。接着,我们再递归对左右两边的数据进行排序,直到区间缩小为 1 ,说明所有的数据都排好了序。
  • 快速排序是一个原地排序算法,是一个不稳定的排序算法,因为其在数据交换过程中可能会改变相等元素的原始位置。

区别:

  • 归并排序是个稳定排序,他的时间复杂度为O(nlogn)
  • 快速排序是不稳定排序,最坏情况下时间复杂度为O(n^2),但平均情况下时间复杂度为O(nlogn)

编程题1:两个三十六进制数相加

编程题2:实现一个flattern函数

function flattern(arr) {
  let res = [];
  const flat = function (arr) {
    for(let i = 0; i < arr.length; i++) {
      if(Array.isArray(arr[i])) {
        flat(arr[i]);
      } else {
        res.push(arr[i]);
      }

    }
  }
  flat(arr);
  return res;
}

编程题3:实现一个函数sum(1,2)(3)(4).sumOf() // 10

function sum() {
  let args = [].slice.call(arguments);
  const fn = function() {
    let bindArg = [].slice.call(arguments);
    [].push.apply(args, bindArg);
    return fn;
  }
  fn.sumOf = function () {
    return args.reduce((pre, cur) => {
      return pre + cur;
    })
  }
  return fn;
}

小结

去年也体验过字节的面试,字节的面试一如既往注重coding,而coding这块也是我还比较欠缺的,这些编程题你或许在别的面经里也看到过,但他的解法有很多,每个人的实现可读性和性能是有差异的,最后二面面试官也给我了很好的建议,面对一段代码,你应该尝试用不同的方法去解,不停探究他的更优解法,这才是能真正提升自己编程能力的途径。

涂鸦

一面

面试流程

  • 自我介绍
  • 项目深挖
  • 基础知识
  • 提问环节

面试题目

BFC是什么,怎么触发BFC?

BFC、Flex box,一文带你探索CSS布局定位的奥秘

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

JS中观察者模式与发布订阅模式

Vue双向绑定的实现原理?

这个涉及到vue2.0和3.0的不同实现。

介绍简历上的项目

如何处理跨域问题

跨域问题及其解决方式

二面

面试流程

  • 自我介绍
  • 项目深挖
  • 基础知识
  • 提问环节

面试题目

介绍简历上的项目

讲讲XSS、CSRF、SQL inject攻击

XSS、CSRF攻击问题详解

vue是怎么做组建通信的

如何实现Vue组件化

讲讲vue的底层实现

用webpack可以做哪些优化?

  • OptimizeCssAssetsWebpackPlugin:压缩css
  • babel缓存
  • 使用ES6 modules可以实现tree-shaking
  • splitChunks:抽取入口文件的公共方法到单独的chunk
  • 多进程打包
  • externals:拒绝打包,可以使用外部CDN
  • DllReferencePlugin:第三方的库不参与打包

三面

面试题目

三面的面试官当时比较忙,稍微等了一会面试也比较匆忙,面试官看我前两轮技术上的问题基本都问的差不多了,就没再多问,基本属于闲谈,聊了我前几段工作经历和离职原因,也问了以后的职业规划,发展方向,还问了我的学习方法。

小结

涂鸦是我这次面试里唯一一个线下面试的,一次性完成三轮技面,因为没有及时记录复盘,所以有些题目已经记不太清,总共聊了一个半小时左右,因为是线下的面试,所以没有涉及手撕代码,但是简历上的点全都问到了,大部分时间都是围绕你简历上的项目展开知识点,这也应证了简历就是你的考纲,对于你所列出的点,你都要认真去理解思考,举一反三。

快手

一面

面试流程

  • 自我介绍
  • 基础知识
  • 项目深挖
  • 提问环节

面试题目

webpack打包过程

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始
  3. 执行编译;确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 webpack

这个步骤中没有提到plugins,实际上,webpack在打包过程中会在不同阶段广播一些事件钩子,不同的plugins有自己作用的时间,当听到这些钩子时,将开始执行。

less在webpack中时如何被打包的?

less-loader -> css-loader -> style-loader

  • less-loader:用于将less翻译成浏览器可识别的css
  • css-loader:处理 import / require() @import / url 引入的内容
  • style-loader:通过一个JS脚本创建一个style标签,里面包含一些样式

loader和plugins有什么区别?

  • loader:是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
  • plugin:是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

有了解过tapable吗?

Webpack tapable 使用研究

了解小程序的双线程架构吗?

这一部分确实没怎么了解过,大家可以看看官方文档

微信小程序的双线程架构

谈谈你对HTTP状态码304的认识

这个其实就是协商缓存时缓存依旧生效的状态码,有关浏览器缓存可以看看我以前写的这篇文章

浏览器缓存机制

vue实现双向绑定过程

建议大家看看vue的源码,在vue2.0和vue3.0中实现还是不太一样的,2.0的实现也可以看看我写的这篇文章

vue源码学习知识梳理

vue数组是如何实现双向绑定的(push, pop等方法)

数组的双向绑定需要将依赖收集dep绑到每个元素上,除此之外,针对数组原生的诸如push,pop等方法,则是通过重写Array原型链上的这些方法,下面贴下vue里的源码,方便大家理解。

// src/core/observer/array.js

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

实现一个两列布局

new做了什么?实现一个new过程

function new(constructor, arg) {
  let obj = Object.create(constructor);
  let res = constructor.apply(obj, args);
  return typeof res === 'object' ? res : obj;
}

判断输出

Function.prototype.a = () => {
  console.log(1);
}
Object.prototype.b = () => {
  console.log(2);
}
function A() {}
const a = new A();
a.a();
a.b();

a()为undefined,阻塞后b()也没有输出。

实现一个函数isStrValid函数

判断字符串是否是有效字符串,字符串包括 '(',')','{','}','[',']' 、字母和数字

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。
[] => true
[]{} => ture
[[[] => false
[[()}] => false

解答:

function isStrValid(str) {
  let stack = [];
  for(let i = 0; i < str.length; i++) {
    if (str[i] === '(' || str[i] === '{' || str[i] === '[') {
      stack.push(str[i]);
    } else if (str[i] === ')') {
      if (stack[stack.length - 1] === '(') {
        stack.pop();
      } else {
        return false;
      }
    } else if (str[i] === ']') {
      if (stack[stack.length - 1] === '[') {
        stack.pop();
      } else {
        return false;
      }
    } else if (str[i] === '}') {
      if (stack[stack.length - 1] === '{') {
        stack.pop();
      } else {
        return false;
      }
    }
  }
  return stack.length === 0;
}

实现一个EventEmiter类

源码比较长,可以看看我之前写的文章

JS中观察者模式与发布订阅模式

二面

面试流程

  • 自我介绍
  • 项目深挖
  • 基础知识
  • 编码题
  • 提问环节

面试题目

vue3.0和vue2.0的区别

这里聊了挺多,主要还是涉及vue的底层实现,数据响应化处理在2.0和3.0的实现还是相差很大的,建议大家好好看看vue2.0源码和composition api。

vue的data为什么是函数而不直接是对象?

这个在之前滴滴的面试中也有被问到过。

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

common.js和ES6 module有什么区别?

  • CommonJS 模块加载过程是同步阻塞性地加载,在模块代码被运行前就已经写入了 cache,同一个模块被多次 require 时只会执行一次,重复的 require 得到的是相同的 exports 引用。
  • ES6 模块会在程序开始前先根据模块关系查找到所有模块,生成一个无环关系图,并将所有模块实例都创建好,这种方式天然地避免了循环引用的问题,当然也有模块加载缓存,重复 import 同一个模块,只会执行一次代码。
  • CommonJS 可以在运行时使用变量进行 require, 例如 require(path.join('xxxx', 'xxx.js')),而静态 import 语法(还有动态 import,返回 Promise)不行,因为 ES6 模块会先解析所有模块再执行代码。
  • require 会将完整的 exports 对象引入,import 可以只 import 部分必要的内容,这也是为什么使用 Tree Shaking 时必须使用 ES6 模块 的写法。
  • import 另一个模块没有 export 的变量,在代码执行前就会报错,而 CommonJS 是在模块运行时才报错。

requestAnimationFrame和setTimeout有什么区别?

  • 引擎层面:setTimeout 属于 JS 引擎,存在事件轮询,存在事件队列。requestAnimationFrame 属于 GUI 引擎,发生在渲染过程的中重绘重排部分,与电脑分辨路保持一致。
  • 性能层面:当页面被隐藏或最小化时,定时器 setTimeout 仍在后台执行动画任务。当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,requestAnimationFrame 也会停止。
  • 应用层面:利用 setTimeout,这种定时机制去做动画,模拟固定时间刷新页面。requestAnimationFrame 由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,在特定性环境下可以有效节省了CPU 开销

运行下面的函数会有什么情况?

function A() {
  setTimeout(() => {
    A();
  })
}

function A() {
  Promise.resolve().then(() => {
    A();
  })
}

A();

两种都会一直执行直至溢出。

判断下面输出?

a.js
export default const a = {
    
}

b.js
import a from 'a'
a.x = 1;

c.js
import a from 'a'
a.x undefined or 1?

a.x为1,如果使用commonjs,则为undefined

实现一个顺时针打印矩阵

leetcode原题,直接上链接

顺时针打印矩阵

三面

面试流程

  • 自我介绍
  • 项目深挖
  • 编码题
  • 提问环节

面试题目

讲讲V8垃圾回收机制

V8垃圾回收机制

写一个深度遍历。

DFS
        a
  b          c
d   e            g

前:a b d e c g
中:d b e a c g
后:d e b g c a

DFS: a b d e c g
BFS: a b c d e g

解答:

function DFS(root) {
  let stack = [root];
  let res = [];
  while(stack.length) {
    let node = stack.pop();
    res.push(node.val);
    root.right && stack.push(root.right);
    root.left && stack.push(root.left);
  }
  return res;
}

写一个render函数实现以下功能

<div>${a.b}</div>
<div>${a.c.d}</div>

window.a = {
  b: 1,
  c: {
    d() { return 'abc'; }
  }
} 


function render(tplStr,   window) {
  // 
}


<div>1</div>
<div>abc</div>

项目

三面大多时间都在聊我简历上的项目,这是唯一一个认真看了我的博客和github的面试官,当时就特别感动,聊的项目是我github上的一个开源小游戏,聊到了游戏的设计思路,开发过程,开发中的重点难点,因为我的游戏涉及了重构,所以也聊了重构做了些什么,为什么要这么做。

小结

快手和字节差不多,也是特别注重coding,一面基本都是在coding中度过的,所以编码是基础,平时就需要多写多想来不断积累。快手的面试官是真的非常尊重候选人,只要是你简历上展示的,他们都会仔细去看,所以如果大家有留自己的技术博客或者github,最好也都提前回顾一下自己写的内容,不要错过让自己闪光的机会。

总结

从上家公司出来后,我没有急着找工作,在家修养了一个月,总结了一下上份工作中的经验,又看了一些书,刷了些题,因为这次面试我的目标很明确就是大厂,所以也是做了不少准备。不过面试这件事,运气也是实力的一部分,所以大家努力过,在面试的时候就调整好心态,做到无悔自己不留遗憾就可以了,你的努力总有一天会被发现。

这里其实不建议大家频繁跳槽,当然像我一样遇到不可抗外力的,也并非本愿,不过要想沉淀技术,确实需要时间。当然好的平台可以让你提升的更快,所以大家在选择的时候也要多关心团队和技术方向。

最后分享给大家一段乔布斯在斯坦福毕业典礼上我非常喜欢的演讲,希望与大家共勉:

“Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work. And the only way to do great work is to love what you do. If you haven’t found it yet, keep looking. Don’t settle. ”