最近一个月把大厂面了个遍,还未上岸……

简介: 本文由我的读者小伙伴 sensFeng 投稿,这一个月他面了很多家大公司,这份面试经验对最近正在准备找工作的小伙伴可以说是非常有参考价值了,在文章中他也给出了他整理的答案,诚意满满!

前言


笔者两年前端经验,前后大概面了一个月,期间面了很多公司,比如有赞涂鸦智能滴滴字节酷家乐大搜车海康威视税友等等,梳理一下基于我个人面试过程中被问的到的一些问题(包括但不限于)

在开始面试之前,一份优秀的简历也是十分重要,推荐两篇文章:

为了让自己拿到比较满意的 offer,前一周选了一些中小公司来练手,后面发现,太小的公司没有锻炼效果,所以一周后,就开始给规模比较大的厂投简历了。

印象比较深刻的是酷家乐,一面是远程,然后二面约你现场,现场是三轮技术 + hr,差不多三个小时,效率很高,但是第一次面试这么久(当时也不知道要面这么久),导致后面表现的不是很好,遂,卒。涂鸦智能的效率也很高,跟酷家乐的面试流程是一样的,都是远程一面,现场三轮技术 + hr。其他的公司基本上是一轮一轮的来,每一轮的结果一般第二天会告知你,当然,如果后面几天等不到,也是一种告知。

悄悄加一句,欢迎联系 ssh 内推或者交个朋友 😉,微信 sshsunlight


HTTP


http 的问题算是面试热点问题,在一些交叉面或者一面里面很喜欢问,是比较考验你对基础的掌握。


get 和 post 有什么区别?


请求参数:get 请求参数是通过 url 传递的,多个参数以&连接;POST 请求放在 request body 中。

请求缓存:get 请求会被缓存,而 post 请求不会,除非手动设置。

相对的安全性:get 是将参数通过 url 传递的,会被浏览器缓存,容易被他人获取,post 相对来说,比较安全。

请求参数长度限制:get 通过 url 传参,浏览器会限制 url 的长度(http不会)。

编码方式:GET 请求只能进行 url 编码,而 POST 支持多种编码方式。


http1.1 和 http2.0 有什么区别?


(包括但不限于 1.1 和 2.0 的版本对比)

http1.1

  • 引入了持久链接,即 TCP 默认不关闭,可以被多个请求复用
  • 引入管道机制,一个 TCP 连接,可以同时发送多个请求
  • 新增了一些缓存的字段
  • 新增了一些方法,PUT、DELETE、OPTIONS、PATCH
  • 支持断点续传,通过请求头字段 Rang 来实现

http2.0

  • 头部压缩
  • 多路复用
  • 二进制传输,头信息和数据体都是二进制
  • 请求优先级, 设置数据帧的优先级,让服务器优先处理
  • 服务器主动推送消息

被问到的一些问题:

  • 管道机制会造成什么样的问题,http2.0 是怎么解决的
  • 头部压缩的原理是什么
  • options 方法的作用
  • http2.0 允许服务器主动推送消息,那跟 WebSocket 有什么区别吗?

推荐一篇神三元大佬的文章:HTTP 灵魂之问,巩固你的 HTTP 知识体系[2]


说一下 http 缓存


等你说完强缓存协商缓存的大致流程,面试官可能基于你的答案,来深入考察你对 http 缓存的理解,比如(包括但不限于):


200 状态码一定是服务器返回的吗?


不是,命中强缓存的话,会直接从内存或者磁盘中读取资源,并返回一个 200 状态码,具体操作可以试试浏览器的前进后退键。

Expires 和 Cache-Control 的 max-age 指令分别是如何确定过期时间的?优劣势是什么?

为什么有了 Last-Modified 还要有 ETag?解决了什么问题?

no-store 和 no-cache 的意思分别是什么?

推荐一篇文章,解答了这些问题:一张图理解 http 缓存[3]

https 为什么比 http 安全

面试官会让你说下 https 做了什么,然后不排除会问你加密原理。(ps: 字节面试官问了)

主要用到了对称加密和非对称加密,推荐阅读:彻底搞懂 HTTPS 的加密原理[4]

说一下三次握手四次挥手

这个问题问的比较少,只遇到过一次。推荐一篇笔者之前写的文章: TCP 三次握手、四次挥手[5]


JS


0.1 + 0.2 !== 0.3?为什么?


  • 二进制转换:js 在做数字计算的时候,底层都是转二进制来计算的,0.1 转二进制是无限循环小数,0.2 也是,但是 js 采用的IEEE754 二进制浮点运算[6],小数后面只会保留 53 位有效数字,导致精度丢失。
  • 对阶运算:阶小的尾数要根据阶差来右移,尾数位移时可能会发生数丢失的情况,影响精度。

如何解决上面说的精确度丢失问题?


  • 刚开始第一反应就是,先将小数点扩大变成整数,做完加法之后,再除回去。(不妥当,因为还是可能会超过 js 最大数)
  • 利用第三方库,比如Math.jsbig.js
  • 都转成字符串,然后对两个字符串做加法运算(手写题有实现)

闭包


  • 说一下闭包的本质是什么?会造成哪些问题
  • 除了函数的内部返回一个函数,还有其他的方法产生闭包吗?(有)
// 函数内部延迟调用,产生了闭包
function test() {
  let name = "tom";
  setTimeout(() => {
    console.log(name);
  }, 2000);
}
test();

推荐文章:JavaScript 的静态作用域链与“动态”闭包链[7]


什么是作用域?什么是作用域链?函数执行上下文包含了哪些内容?


this 指向


面试官问:JS 的 this 指向[8]


es6 的问题


  • 箭头函数和普通函数的区别
  • 什么是暂时性死区,什么是变量提升
  • for of 和 for in 的区别,怎么让 for of 可以遍历一个对象?
  • es6 的 Map 和 WeakMap 的区别,WeakMap 解决了什么问题?
  • promise 哪些方法是原型上的,哪些方法是实例上的

推荐阮一峰老师的教程:ES6 入门教程[9]


原型 + 原型链 (这个属于必问的题)


这个问题只要能描述的清楚对象如何查找它的值,基本上就理解了一大半。

解释一下上面的图:

  • prototype是函数特有的属性,__proto__是每个对象都有的属性,而prototype本身也是一个对象
  • 当我们去获取a.name的时候,会先从对象的自身属性开始查找,如果没有的话,就会从a.__proto__上找
  • 对象a.__proto__又指向构造器函数testprototype(原型),所以从a.__proto上找属性其实就是在test.prototype找属性,但是prototype(原型)本身又是一个对象,这样的话,会重复上面两个步骤,最终都是找到了Object这个构造器函数,而Object.__proto是一个 null 值,如果中间有值就返回,没有就赋值undefined
  • 这样的链式结构就是原型链

因为构造器函数原型上的constructor是指向构造器函数自身的,所以

a.constructor === test; // true
a.__proto__.constructor === test; // true
a.__proto__.constructor === test.prototype.constructor; // true

有一道面试题可以测试一下,说出打印内容,并且说明原因。

function test() {}
test.prototype.then = function () {
  console.log("test => then");
};
Function.prototype.mythen = function () {
  console.log("Function => mythen");
};
test.mythen();
test.then();


eventLoop


面试官会基于你的回答来提问,比如:

  • 你刚刚说到 js 是单线程,那线程跟进程有什么区别?
  • 浏览器新开了一个页面,有几个线程?
  • 为什么要设计成微任务先执行,宏任务后执行。

推荐一篇酷家乐大佬的文章:这一次,彻底弄懂 JavaScript 执行机制[10]


垃圾回收机制


  • 你刚刚提到的标记清除法有什么缺点?怎么解决?
  • 你刚刚提到的引用计数法有什么缺点吗?
  • v8 里面的垃圾回收机制是什么?
  • v8 是怎么解决循环引用的?

推荐文章:你真的了解垃圾回收机制吗[11]


说一下数据类型,如何判断一个数组


判断数组的方法:

  • Array.isArray(arr); // true
  • arr instanceof Array; // true
  • arr.constructor === Array; // true
  • Object.prototype.toString.call(arr); // "[object Array]"

ps:通过 instanceof 和 constructor 来判断不一定准确,因为可能会被重写。


常用的设计模式?


  • 什么是抽象工厂模式
  • 发布订阅模式和观察者模式有什么区别
  • 你项目里面都用了哪些设计模式

推荐文章:前端需要掌握的设计模式[12]


浏览器渲染过程


一般我都是根据这张图,把流程说一遍。

被面试官问到的一些问题:

  • link 标签会不会阻塞页面的渲染?说一下原因?
  • 为什么 css 推荐放上面,js 推荐放下面?
  • js 会阻塞页面的渲染吗?说一下原因?
  • css 会阻塞 html 的解析吗?为什么?
  • css 会阻塞 html 的渲染吗?为什么?

js 执行是单独的线程,浏览器渲染是GUI渲染线程负责的,两个线程是互斥关系,所以很好理解,js 会阻塞页面的渲染。

css 不会阻塞 html 的解析,解析 html 和解析 css 是并行的,但是 css 会阻塞 html 的渲染,因为页面渲染的时候,需要style Rules + DOM Tree一起合成Render Tree

正常来说,解析页面过程中如果遇到一个 script 标签,会停止 html 解析(如下图),去下载 script 脚本,下载完毕之后立即执行脚本,然后接着解析 html,所以如果 script 下载速度很慢,会造成页面白屏。

defer:html 解析和脚本下载并行(异步),等 html 解析之后,DOMContentLoaded触发之前执行脚本。

async:html 解析和脚本下载并行(异步),下载后立即执行脚本,且中断 html 解析。

推荐阅读:

浏览器的工作原理:新式网络浏览器幕后揭秘[13]

从 8 道面试题看浏览器渲染过程与性能优化[14]


性能优化


我一般从 http 请求 + 代码层面 + 构建工具来回答。


setTimeout 和 requestAnimationFrame 做动画有区别吗?哪一个更好?为什么?


有区别:

  • 准确性

setTimeout做动画不准确,因为是宏任务,设置的延迟时间并不等于在延迟时间之后立即执行,因为需要等待同步任务和微任务执行完,才执行宏任务,容易丢帧;requestAnimationFrame则是在每一帧绘制元素之前执行,更精确。

  • 更好的性能

在隐藏元素或者元素不可见时,setTimeout仍然在后台执行动画任务;而requestAnimationFrame会停止动画,这意味着更少的 CPU 和更少的内存消耗。

介绍一下同源策略?你知道那些跨域方法?cors 跨域的原理是什么有了解过吗?


CSS


介绍一下盒模型?

flex: 1 代表什么意思

用过 flex 布局吗?都有哪些属性?

说说什么是 BFC,一般你都用来干什么,解决了什么问题?

实现元素水平垂直居中?尽可能说多一些方法?

左侧固定 + 右侧自适应布局?说说几种方案?

重绘和重排?

推荐文章:重排(reflow)和重绘(repaint)[15]


React


面了一圈下来,发现 react 问的都差不多。

都用过那些版本的 react,分别介绍一下区别?

说一下前端路由,他们的实现原理分别是什么?

hash 路由:通过监听hashchange事件来捕捉 url 的变化,来决定是否更新页面。

history 路由:主要监听popStatepushStatereplaceState来决定是否更新页面。但是要注意,仅仅调用pushState方法或replaceState方法 ,并不会触发popState事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用backforwardgo方法时才会触发,想要pushStatereplaceState的时候触发popState事件,需要自定义事件。

你能手写个简单的 redux 吗?

面试官说完之后,给我递过来笔和纸......(内心崩溃) 写完了之后,面试官让我给他讲一遍。

function createStore(reducer) {
  let listeners = [];
  let currentState;
  function getState() {
    return currentState;
  }
  function dispatch(action) {
    currentState = reducer(currentState, action);
    listeners.forEach((l) => l());
  }
  function subscribe(fn) {
    listeners.push(fn);
    return function unsubscribe() {
      listeners = listeners.filter((l) => l !== fn);
    };
  }
  return {
    getState,
    dispatch,
    subscribe,
  };
}

redux 里面 dispatch 是如何正确找到 reducer 的?

combineReducers 源码[16]

是的,redux 它不找,一把梭,每次 dispatch 都要遍历一遍所有的 reducer...

redux 怎么挂载中间件的?它的执行顺序是什么样的?

核心函数compose

function compose(middlewears) {
  return middlewears.reduce(
    (a, b) =>
      (...argu) =>
        a(b(...argu))
  );
}

执行顺序:从后往前执行 redux 中间件。

除了 redux,还用过其他的状态管理库吗?

没有。

redux 的缺点?

react 生命周期?

setState 什么情况下同步,什么情况下异步?

讲一下 react 的事件机制,为什么这么设计?react17 里面有什么变化吗?

推荐文章:一文吃透 react 事件系统原理[17]

设计的好处

  • 抹平浏览器差异,实现更好的跨平台
  • 避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁
  • 方便事件统一管理和事务机制

class 组件跟函数组件有什么区别?

能在 if 判断里面写 hooks 吗?为什么不能?

Hooks 是用链表保存状态的,每次渲染的时候,必须要保证 hooks 的长度和顺序是一样的,如果不一致,react 无法获取正确状态,会报错。

HOC 和 hooks 的区别?

hooks 实现原理?不用链表可以用其他方法实现吗?

基于链表来实现的,也可以用数组来模拟。

useEffect 依赖传空数组和 componentDidMount 有什么区别吗?

useeffect 和 useLayouteffect 区别

  • useEffect不会阻塞浏览器渲染,而  useLayoutEffect  会阻塞浏览器渲染。
  • useEffect会在浏览器渲染结束后执行,useLayoutEffect  则是在  DOM  更新完成后,浏览器绘制之前执行。

介绍一下 react dom diff

介绍一下 Vdom?

在 React 中,有做过什么性能优化吗?

按照自己脑袋瓜里面的想法,说了几种。

推荐文章:React 性能优化的 8 种方式了解一下[18]

React.memo()和 React.useMemo()有什么区别吗?

接上题,然后面试官这两个有什么区别吗?

直接上官网: React.memo[19]React.useMemo[20]

useCallback 和 useMemo 的区别?

同接上题 官网地址 useCallback[21]

React.fiber 了解吗?造成卡顿的原因是什么?react.fiber 里面是怎么解决的?中断更新之后,下次是如何找到要更新的位置的?

推荐荒山大佬的文章:这可能是最通俗的 React Fiber(时间分片) 打开方式[22]

函数式编程

讲 hooks 很大概率问到函数式编程?(被问到过好几次)

  • 说一下你理解的函数式编程?
  • 有哪些库是利用了函数式编程的思想?
  • lodash 是真正的函数式编程库吗?

简明 JavaScript 函数式编程——入门篇[23]

高阶组件里面如何防止 ref 丢失?

React.forwardRef,上官网地址:React.forwardRef[24]


webpack && node


  • 都用过 node 干了什么?用过 node 框架吗?
  • node 一些基本 api,如何删除文件,创建文件,写入文件。
  • 用过 node 的哪些模块?

进程和线程的区别

介绍一下模块发展史

node_modules 问题

假如 npm 安装了一个模块 A、依赖 c 的 0.0.1 版本,又安装了一个模块 B,依赖 c 的 0.0.2 版本。请问 node_module 是怎么保证 A、B 正确的找到对应的 c 版本的包的?

webpack 打包原理

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

webpack 性能优化你是怎么做的?

推荐文章:带你深度解锁 Webpack 系列(优化篇)[25]

loader 和 plugin 的区别?

  • loader(转换):主要是做转换功能,比如将 css、less 文件转成 js,识别不同的文件后缀名,可以拓展一下自己比较熟悉的 loader。
  • plugin(插件):原理是监听 webpack 构建过程中的一些钩子,然后做一些自己的操作,更多的是丰富 webpack 的功能。

loader 的执行顺序是什么?如何写一个 loader?如何写一个 plugin?loader 有异步的吗?

执行顺序从右往左、从下到上。

loader 本质是一个函数,plugin 本质是一个类,具体如何编写,推荐文章:手把手教你写一个 loader / plugin[26]

loader 有同步的也有异步。

file-loader 返回的是什么?

返回的是一个字符串,详情见:file-loader 源码地址[27]

webpack 有几种 hash,它们有什么区别?一般你在项目里面是用哪种 hash?

  • hash:是整个项目的 hash 值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 发生改变。
  • chunkHash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值(来源于同一个 chunk,则 hash 值就一样)。
  • contentHash:根据文件内容生成 hash 值,文件内容相同 hash 值就相同

webpack5 有哪些新特性?

推荐文章:飞书团队 Webpack5 上手测评[28]

webpack 热更新原理?

推荐文章:轻松理解 webpack 热更新原理[29]

tree-shaking 原理?

利用ES Module做静态分析,通过分析 ast 语法树,对每个模块维护了一个作用域,收集模块内部使用的变量,然后分析作用域,将import进来未被使用的模块删除,最后递归处理文件。

babel 转换代码的过程,箭头函数转普通函数的时候,是如何处理 this 的?

过程:parser => transfrom => generator,可以根据自己的理解,展开说说。

箭头函数转普通函数如何处理 this:就近找上一层作用域里面的 this,用一个唯一变量名 that 缓存一下 this,然后将之前箭头函数里面的 this 替换成 that 即可。


手写题


节流和防抖


(某独角兽公司)面试官:你能手写个节流函数吗?然后递过来了笔跟纸......


函数防抖


定义:在 n 秒时间内,函数只会触发一次,如果期间被触发,则重新计时。

场景input框实时搜索浏览器的resize、和scroll

function debounce(fn, delay) {
  let timer;
  return function (...argu) {
    let that = this;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(that, argu);
    }, delay);
  };
}


函数节流


定义:在 n 秒内,事件只执行一次,如果期间被触发,也不会响应事件。

场景表单重复提交滚动加载

// 利用时间戳实现
function throttle(fn, delay) {
  let previous = new Date();
  return function (...argu) {
    let nowDate = new Date();
    if (nowDate - previous > delay) {
      fn.apply(this, argu);
      previous = nowDate;
    }
  };
}
// 利用定时器实现
function throttle(fn, delay) {
  let timer;
  return function (...argu) {
    let that = this;
    if (timer) return false;
    timer = setTimeout(() => {
      fn.apply(that, argu);
      timer = null; // 释放timer变量,让下一次的函数接着执行
    }, delay);
  };
}


写一种你熟悉的排序?


没错,也是用笔写,写完了然后给他讲思路。我选了一个快速排序

// 先选一个数当作基点,一般选择最后一个数
// 然后遍历arr, 找出这个基点数的比它大的数组集合和比它小的数组集合
// 递归此步骤
function quickSort(arr) {
  if (arr.length < 2) {
    return arr;
  }
  const cur = arr[arr.length - 1];
  let left = [];
  let right = [];
  for (let i = 0; i < arr.length - 1; i++) {
    if (arr[i] >= cur) {
      right.push(arr[i]);
    } else {
      left.push(arr[i]);
    }
  }
  return [...quickSort(left), cur, ...quickSort(right)];
}
console.log(quickSort([1, 3, 3, 6, 2, 4, 1]));


(字节)说出打印顺序


默认非严格模式

function Foo() {
  getName = function () {
    alert(1);
  };
  return this;
}
Foo.getName = function () {
  alert(2);
};
Foo.prototype.getName = function () {
  alert(3);
};
var getName = function () {
  alert(4);
};
function getName() {
  alert(5);
}
// 请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();

正确输出顺序:2 4 1 1 2 3

  • Foo.getName();这个没什么好说的,输出 2
  • getName();考察 var 和函数提升,函数优先级大于 var,所以输出 4
  • Foo().getName();Foo()返回 this,此时 this 指向 window,Foo().getName 相当于 window.getName。但是 Foo()内部又对 window 上的 getName 重新赋值了,所以输出 1
  • getName();同上,输出 1
  • new Foo.getName();考察运算符优先级[30],new 无参数列表,对应的优先级是 18;成员访问操作符 . , 对应的优先级是 19。因此相当于是 new (Foo.getName)();new 操作符会执行构造函数中的方法,因此此处输出为 2.
  • new Foo().getName();new 带参数列表,对应的优先级是 19,和成员访问操作符.优先级相同。同级运算符,按照从左到右的顺序依次计算。new Foo()先初始化 Foo 的实例化对象,实例上没有 getName 方法,因此需要原型上去找,即找到了 Foo.prototype.getName,输出 3

instanceof 原理,能尝试手写一个 instanceof 函数吗


function myInstanceOf(left, right) {
  if (left === null || typeof left !== "object") return false;
  if (typeof right !== "function") throw new Error("right must be function");
  let L = left.__proto__;
  let R = right.prototype;
  while (L !== null) {
    if (L === R) return true;
    L = L.__proto__;
  }
  return false;
}


实现 new 关键字


  • 创建一个空的简单 javaScript 对象,即{}
  • 将创建对象的__proto__指向构造函数的prototype
  • 修改this指向
  • 如果构造函数没有返回值,就返回创建的对象。
function myNew(context, ...argu) {
  let obj = Object.create(null);
  obj.__proto = context.prototype;
  let res = context.apply(obj, argu);
  return typeof res === "object" ? res : obj;
}


大数相加


let a = "123456789012345678";
let b = "1";
function add(a, b) {
  //...
}
add(a, b); // '123456789012345679'

思路:模拟加法运算,但是需要用'0'补齐长度,对于整数,向前补 0。

let a = "123456789012345678";
let b = "1";
// 1. 先找出最大的长度的数
// 2. 给较小的数填充向前填充0
function add(a, b) {
  let maxLength = Math.max(a.length, b.length);
  a = a.padStart(maxLength, "0");
  b = b.padStart(maxLength, "0");
  // 123456789012345678
  // 000000000000000001
  let res = ""; // 返回的值
  let sum = 0; // 同位相加的和
  let t = 0; // 同位相加和的十位数
  let r = 0; // 同位相加和的个位数
  for (let i = maxLength - 1; i >= 0; i--) {
    sum = parseInt(a[i]) + parseInt(b[i]) + t;
    t = Math.floor(sum / 10); // 拿到十位数的值
    r = sum % 10; // 拿到个位数的值
    res = r + res;
  }
  return res;
}
console.log(add(a, b)); // 123456789012345679


(滴滴)扁平化数组


实现一个 flat 函数,接收 arr 和 depth 参数

function flat(arr, depth = 1) {
  return depth > 0
    ? arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
      }, [])
    : arr.slice();
}


实现一个 event 类


class EventEmitter {
  constructor() {}
  // 监听事件
  on() {}
  // 触发事件
  emit() {}
  // 只监听一次,下次emit不会触发
  once() {}
  // 移除事件
  off() {}
}
const events = new EventEmitter();
events.on("hobby", (...argu) => {
  console.log("打游戏", ...argu);
});
let eat = () => {
  console.log("吃");
};
events.once("hobby", eat);
events.on("hobby", (...argu) => {
  console.log("逛街", ...argu);
});
events.off("hobby", eat);
events.emit("hobby", 1, 2, 3);
events.emit("hello", "susan");
//打游戏 1 2 3
// 逛街 1 2 3

答案:

class EventEmitter {
  constructor() {
    this.events = {}; // 存放着所有的事件{eventName: [callback, ...]}
  }
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [callback];
    } else {
      this.events[eventName].push(callback);
    }
  }
  emit(eventName, ...argu) {
    if (this.events[eventName]) {
      this.events[eventName].forEach((fn) => fn(...argu));
    }
  }
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        (fn) => callback !== fn && fn.l !== callback
      );
    }
  }
  once(eventName, callback) {
    const _once = () => {
      callback();
      this.off(eventName, _once);
    };
    _once.l = callback;
    this.on(eventName, _once);
  }
}


实现千分位 format 函数


// 接收一个number,返回一个string
function format(number) {}
console.log(format(12345.789)); // 12,345.789,0
console.log(format(0.12345678)); // 0.123,456,78
console.log(format(123456)); // 123,456

思路

  • 基于小数点切分,对于整数部分,从后往前遍历,隔 3 加,
  • 小数点部分,从前往后便利,隔 3 加,
function format(number) {
  let str = number.toString();
  let [int, dec = ""] = str.split(".");
  let intStr = "";
  for (let i = int.length - 1; i >= 0; i--) {
    if ((int.length - i) % 3 === 0 && i !== 0) {
      intStr = "," + int[i] + intStr;
    } else {
      intStr = int[i] + intStr;
    }
  }
  let decStr = "";
  if (dec.length > 0) {
    for (let i = 0; i < dec.length; i++) {
      let sum = decStr + dec[i];
      if ((i + 1) % 3 === 0) {
        decStr = sum + ",";
      } else {
        decStr = sum;
      }
    }
  }
  return decStr.length > 0 ? `${intStr}.${decStr}` : `${intStr}`;
}

(字节、滴滴)根据传入的姓名权重信息,返回随机的姓名(随机概率依据权重)

第一次没看懂题目,面试官解释了一下。

/**
 * @description: 根据传入的姓名权重信息,返回随机的姓名(随机概率依据权重)
 * @param {Array} personValue
 * @returns {String} personName 姓名
 */
var getPersonName = function (personValue) {};
const person = [
  {
    name: "张三",
    weight: 1,
  },
  {
    name: "李四",
    weight: 10,
  },
  {
    name: "王五",
    weight: 100,
  },
];
function getResult(count) {
  const res = {};
  for (let i = 0; i < count; i++) {
    const name = getPersonName(person);
    res[name] = res[name] ? res[name] + 1 : 1;
  }
  console.log(res);
}
getResult(10000);

答案:

var getPersonName = function (personValue) {
  // 标记区间,并且获得weight的总数
  let sum = personValue.reduce((pre, cur) => {
    cur.startW = pre;
    return (cur.endW = cur.weight + pre);
  }, 0);
  let s = Math.random() * sum; // 获得一个 0 - 111 的随机数
  // 判断随机数的所属区间
  let person = personValue.find((item) => item.startW < s && s <= item.endW);
  return person.name;
};


(字节)实现一个 promise.all


Promise.all = function (promises) {
  let result = [];
  let count = 0;
  return new Promise((resolve, reject) => {
    promises.forEach((p, index) => {
      // 兼容不是promise的情况
      Promise.resolve(p)
        .then((res) => {
          result[index] = res;
          count++;
          if (count === promises.length) {
            resolve(result);
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  });
};


(滴滴)两数之和


力扣原题:两数之和[31]

刚开始用双重循环写了一个,时间复杂度是 O(n^2),面试官问你能优化到 O(n)吗?然后有了下面这个。

var twoSum = function (nums, target) {
  let len = nums.length;
  let map = new Map();
  for (let i = 0; i < len; i++) {
    if (map.has(target - nums[i])) {
      return [map.get(target - nums[i]), i];
    } else {
      map.set(nums[i], i);
    }
  }
};
console.log(twoSum([2, 7, 11, 15], 9));


(字节)无重复最长子串


力扣原题:无重复最长子串[32]

var lengthOfLongestSubstring = function (s) {
  let arr = [];
  let max = 0;
  for (let i = 0; i < s.length; i++) {
    let index = arr.indexOf(s[i]);
    if (index !== -1) {
      arr.splice(0, index + 1);
    }
    arr.push(s.charAt(i));
    max = Math.max(arr.length, max);
  }
  return max;
};


实现 new Queue 类


new Queue()
.task(1000,()=>console.log(1))
.task(2000,()=>console.log(2))
.task(3000,()=>console.log(3)).start();
实现一个Queue函数,调用start之后,1s后打印1,接着2s后打印2,然后3s后打印3

答案

function sleep(delay, callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      callback();
      resolve();
    }, delay);
  });
}
class Queue {
  constructor() {
    this.listenser = [];
  }
  task(delay, callback) {
    // 收集函数
    this.listenser.push(() => sleep(delay, callback));
    return this;
  }
  async start() {
    // 遍历函数
    for (let l of this.listenser) {
      await l();
    }
  }
}
new Queue()
  .task(1000, () => console.log(1))
  .task(2000, () => console.log(2))
  .task(3000, () => console.log(3))
  .start();


总结


还出了很多场景题,让你给出解决方案,中间经历的太久了,面试记录做的不够好,很多问题都忘了。一面基本上都是问基础,后面几轮面试会深入项目和要你给出解决方案。这次面试也是发现了自己的不足,平时写代码的过程中,思考的不够多,希望今后能多一些思考。


参考资料


[1]

一份优秀的前端开发工程师简历是怎么样的: https://www.zhihu.com/question/23150301/answer/1229870117

[2]

HTTP 灵魂之问,巩固你的 HTTP 知识体系: https://juejin.cn/post/6844904100035821575#heading-100

[3]

一张图理解 http 缓存: https://segmentfault.com/a/1190000015816331

[4]

彻底搞懂 HTTPS 的加密原理: https://zhuanlan.zhihu.com/p/43789231

[5]

TCP 三次握手、四次挥手: https://juejin.cn/post/6844904194764177416

[6]

IEEE754 二进制浮点运算: https://www.h-schmidt.net/FloatConverter/IEEE754.html

[7]

JavaScript 的静态作用域链与“动态”闭包链: https://juejin.cn/post/6957913856488243237

[8]

面试官问:JS 的 this 指向: https://juejin.cn/post/6844903746984476686

[9]

ES6 入门教程: https://es6.ruanyifeng.com/

[10]

这一次,彻底弄懂 JavaScript 执行机制: https://juejin.cn/post/6844903512845860872

[11]

你真的了解垃圾回收机制吗: https://juejin.cn/post/6981588276356317214

[12]

前端需要掌握的设计模式: https://juejin.cn/post/6874906145463468046

[13]

浏览器的工作原理:新式网络浏览器幕后揭秘: https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/

[14]

从 8 道面试题看浏览器渲染过程与性能优化: https://juejin.cn/post/6844904040346681358

[15]

重排(reflow)和重绘(repaint): https://juejin.cn/post/6844904083212468238

[16]

combineReducers 源码: https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts

[17]

一文吃透 react 事件系统原理: https://juejin.cn/post/6955636911214067720

[18]

React 性能优化的 8 种方式了解一下: https://juejin.cn/post/6844903924302888973

[19]

React.memo: https://zh-hans.reactjs.org/docs/react-api.html#reactmemo

[20]

React.useMemo: https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo

[21]

官网地址 useCallback: https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback

[22]

这可能是最通俗的 React Fiber(时间分片) 打开方式: https://juejin.cn/post/6844903975112671239

[23]

简明 JavaScript 函数式编程——入门篇: https://juejin.cn/post/6844903936378273799

[24]

React.forwardRef: https://zh-hans.reactjs.org/docs/react-api.html#reactforwardref

[25]

带你深度解锁 Webpack 系列(优化篇): https://juejin.cn/post/6844904093463347208

[26]

手把手教你写一个 loader / plugin: https://juejin.cn/post/6976052326947618853

[27]

file-loader 源码地址: https://github.com/webpack-contrib/file-loader/blob/master/src/index.js

[28]

飞书团队 Webpack5 上手测评: https://juejin.cn/post/6844904169405415432

[29]

轻松理解 webpack 热更新原理: https://juejin.cn/post/6844904008432222215

[30]

运算符优先级: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FJavaScript%2FReference%2FOperators%2FOperator_Precedence

[31]

两数之和: https://leetcode-cn.com/problems/two-sum/

[32]

无重复最长子串: https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

相关文章
|
9月前
|
API UED
推特「崩了」:不登录不让看、推文数量严格设上限,马斯克反复横跳
推特「崩了」:不登录不让看、推文数量严格设上限,马斯克反复横跳
436 0
|
11月前
|
缓存 移动开发 前端开发
字节前端二面凉凉记录,晋级赛失败
面试日期为 2021-06-06 18:00 接着上回一面后,有个人给我打电话了,问我可不可以二面,我毕竟抱着学习的态度来面试的,但是万一成了呢,我突然紧张了。感觉答应的唐突了,但是感觉没事,毕竟滴滴二面时那种八股文我已经又准备了一遍。
|
12月前
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事
47 0
|
12月前
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事(三)
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事(三)
42 0
|
12月前
|
人工智能
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事(二)
关于临时HY学长被安排拉二分题不想翻译找到DYM学长这件事(二)
52 0
|
C++ 索引 容器
刷爆力扣之数组的度
刷爆力扣之数组的度
刷爆力扣之较大分组的位置
刷爆力扣之较大分组的位置
刷爆力扣之寻找数组的中心下标
刷爆力扣之寻找数组的中心下标
【蓝桥杯省赛】冲刺练习题【超大数】倒计时【14】天
【蓝桥杯省赛】冲刺练习题【超大数】倒计时【14】天
154 0