记录下遇到的一些面试题

今年辞职后面试遇到的一些面试题,在这里记录下,放便查阅学习

一些面试题目

题目并不是所有都给出了答案,请自行总结~;另,感谢各位大佬的面经收获颇多;题目可能出现记忆偏差,答案如有错误或者描述不准确,欢迎指正

js是单线程语言,所有任务只能在一个线程上完成,一次只能做一件事。js通过eventLoop来实现异步。

为什么js设计成单线程?

JavaScript 的主要用途是与用户互动,以及操作 DOM。多线程操作DOM的话容易造成混乱(比如一个线程要修改这个DOM,而另一个线程则要删除这个DOM…)。

js 中的 EventLoop

主要需要理解任务(task)微任务(micro task)

常见的任务(task): setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等

常见的微任务(micro task): process.nextTick、new Promise().then(回调)、MutationObserver(html5 新特性) 等

Node 中的 EventLoop

Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,事件循环在其中实现。与浏览器中的事件循环是不同的

在 libuv 中,将事件循环分为六个阶段:

  1. timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
  2. pending callbacks 阶段:该阶段对某些系统操作(如TCP错误类型)执行回调。处理一些上一轮循环中的少数未执行的 I/O 回调
  3. idle, prepare 阶段:仅 node 内部使用
  4. poll 阶段:获取新的 I/O 事件,执行与 I/O 相关的回调,适当的条件下 node 将阻塞在这里
  5. check 阶段:执行 setImmediate() 的回调
  6. close callbacks 阶段:执行 socket 的 close 事件回调

Node 事件循环执行顺序:外部输入 -> poll -> check -> close callbacks -> timers -> I/O callbacks -> idle,prepare -> poll -> …(循环)

重点阶段详解:

  1. timers:Node 会检查有无过期的timer,如果有,则把他的回调加入到timer的任务队列中等待执行「这个过期检测不一定靠谱,受机器上其它运行程序影响,或者那个时间点主线程不空闲」。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 下列代码中 settimeout和setimmediate的执行顺序不确定
    // 但是如果将他们放入到I/O回调中
    // 则 setImmediate会先执行
    // 请参看前面Node中事件循环执行顺序
    setTimeout(() => {
    console.log('timeout')
    }, 0)
    setImmediate(() => {
    console.log('immediate')
    })
  2. poll:该阶段系统会做两件事:

    1. 计算应该阻塞和轮询 I/O 的时间(控制timer阶段的执行)
    2. 处理 poll 队列里的事件

    进入本阶段,同时未设定 timer 时,会发生以下两种情况:

    • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或达到系统限制。
    • 如果 poll 队列为空
      • 如果有预设的 setimmediate() ,则poll阶段会结束并进入执行 check 阶段任务队列
      • 如果没有预设的 setimmediate() ,则EventLoop会被阻塞在该阶段。为防止一直阻塞,该阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。
  3. check:setImmediate()的回调会被加入 check 队列中。check 阶段的执行顺序在 poll 阶段之后。

process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()

参考文章

Node中和浏览器中EventLoop的差异:

区别:microtask 队列的执行时机不同

  • Node.js 中 EventLoop 分为六个阶段
  • 浏览器环境下:microtask 队列是在每个 macrotask 队列执行之后执行。
  • Node.js 中:一个阶段执行完毕就会去执行 microtask 队列的任务。

Vue3.0 中的 Proxy 和 Vue2.x 中的 defineProperty 的优劣对比

结论:

  • Proxy 监听是的对象,而不是对象上的属性(defineProperty 监听的是对象上的属性)
  • Proxy 除了拦截 set 和 get 还能拦截 apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
  • Proxy返回的是一个新对象,可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Object.defineProperty 支持 IE9,Proxy的存在浏览器兼容性问题,而且无法用polyfill磨平。

Vue2.x 的核心函数是 Object.defineProperty,通过给对象属性添加一些配置,拦截属性的 getset 方法来实现数据的响应式绑定,但是对对象新增的属性无能为力,对数组也需要拦截它的原型方法来实现响应式,不能通过 arr[1]=xx 方式来修改数组。此时,vue 提供了一个api:this.$set 使新增属性也能实现响应式。

1
2
3
4
5
6
7
8
9
10
11
12
// vue2中是根据具体的 key 去进行拦截的,需要知道拦截的 key 是什么,
// 所以对新增的key 无能为力。
// 数组不能通过 arr[1]=xx 来修改
let data = {}
Object.defineProperty(data, key, {
get: function() {

},
set: function() {

}
})

Vue3 中使用 Proxy 代替了 Object.defineProperty

1
2
3
4
5
6
7
8
// 这种拦截方式不需要知道具体的 key
// 可拦截任意的 key
// 不管新增的还是已有的
// 数组可以通过 arr[1]=xx 来修改
new Proxy(data, {
get() {},
set(){}
})

参考文章

TCP三次握手以及HTTP位于那一层?(计算机网络)

TCP/IP 协议族分为 4 层:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 数据链路层

三次握手:

  1. 第一次握手:发送端将 SYN=1 同步序列号和初始化序列号 seq=x 发送给接收端
  2. 第二次握手:接收端收到 SYN 请求报文之后,如果同意连接,会以自己的同步序列号SYN(服务端) = 1、初始化序列号 seq = y和确认序列号(期望下次收到的数据包)ack = x+ 1 以及确认号ACK = 1报文作为应答
  3. 第三次握手:接收端接收到服务端的 SYN + ACK 之后,发送同步序列号 ack = y + 1和数据包的序列号 seq = x + 1以及确认号ACK = 1,握手结束。

四次挥手:

  1. 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态
  2. 第二次挥手:服务器端收到FIN后,发送ack=M+1,服务端进入CLOSE-WAIT(关闭等待)状态,客户端进入FIN_WAIT_2 状态
  3. 第三次挥手:服务器端确定数据已发送完成,则向客户端发送FIN=N报文,服务器端进入LAST_ACK状态
  4. 第四次挥手:客户端收到FIN=N报文后,发送ack=N+1后进入TIME_WAIT状态,服务器端收到ACK后断开连接,客户端2SML(最长报文段寿命)后未收到回复则关闭连接,完成四次挥手

为什么是4次挥手?

服务端是分开发送 ack 和 FIN 信息的,所以会多一次。

HTTP 协议属于应用层

HTTPWebsoket 联系与区别

HTTP 与 Websocket 的相同点

  • 位于应用层,基于TCP,需要三次握手四次挥手
  • 使用Request/Response模型进行连接的建立
  • 在连接的建立过程中对错误的处理方式相同(可能返回相同的错误码)

HTTP 与 Websocket 的不同点

  • HTTP 是单向的(客户端发送请求,服务端发送响应);Websocket 是双向的(服务端客户端都可以主动发起请求);
  • HTTP 是无状态协议(每次请求都会新建与服务器的连接,获得响应后自动终止);Websocket 是有状态协议(客户端和服务器之间的连接将保持活动状态,直到被任何一方(客户端或服务器)终止);

webpack 的 loader 和 plugin 的区别

  • Loader:加载器、转换器。因为 webpack 只能解析 js 文件,loader 能让webpack 拥有加载和解析非 js 资源的能力。
  • Plugin:插件、扩展器。扩展 webpack 功能,Plugin 可以监听 webpack 运行期间广播出的一些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

websoket 同时接入的数据量大时性能问题

这个还真不清楚,公司这个项目用户数不多,所以根本就不存在高并发的情况。。个人觉得,这种并发问题和前端关系应该不大,是需要后端处理的。唯一可能与性能有关的前端问题应该是大量、频繁更新前端界面的时候引起的。

这个问题感觉可以和性能优化放在一起

性能优化

HTTP 优化

两个大方向,就是我们通常所说的资源的压缩与合并。这里就需要知道webpack了。

  • 减少请求次数
  • 减少每次请求花费的时间

webpack性能优化:主要两个方面:

  1. webpack 构建过程时间过长
  2. webpack 打包结果体积过大

针对构建时间过长:

  • 不要让loader做太多事
    • 使用 include 或者 exclude 来避免不必要的转译
    • 开启缓存,将转译结果缓存至文件系统
      1
      2
      // 以babel-loader为例
      loader: 'babel-loader?cacheDirectory=true'
  • 第三方库单独打包
    • 处理第三方库的包有:ExternalsCommonsChunkPluginDllPlugin等(详细情况自己再下去研究),这样的第三方库就不会随业务代码被重新打包,只有第三方库自身版本发生变化时才会重新打包

针对打包结果体积过大:

  • 拆分资源。使用 DllPlugin
  • 删除冗余代码
    • Tree-Shaking:是基于 import/export 语法,没有被真正使用的模块在打包的时候会被删除。
    • UglifyJsPlugin:在压缩过程中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除
  • 按需加载

图片优化

JPEG/JPG

特点:有损压缩、体积小、加载快、不支持透明,24位的JPG可存储1600万种颜色

使用场景:适用于呈现色彩丰富的图片。常见的大背景图、轮播图、banner图等

PNG-8 与 PNG-24

PNG 图片具有比 JPG 更强的色彩表现力,弥补了 JPG 的不足,但是体积较大

特点:无损压缩、质量高、体积大、支持透明,8位支持256中颜色,24位支持1600万中颜色

使用场景:Logo、背景简单且对比强烈的图片或背景

SVG

SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述,与其他图片有本质的不同。可以被非常多的工具读取和修改,具有较强的灵活性。

特点:文本文件、体积小、不失真、兼容性好

Base64

作为小图标解决方案而存在的,作用和雪碧图类似

特点: 文本文件、依赖编码、小图标解决方案

存储优化

浏览器缓存机制:

  1. Memory Cache
  2. Service Woker Cache
  3. HTTP Cache
  4. Push Cache
HTTP Cache

分为强缓存协商缓存强缓存优先级较高,强缓存命中失败才会走协商缓存

强缓存:由 HTTP 请求头中的 ExpireCache-Control 两个字段控制。浏览器根据这两个字段判断是否命中强缓存,若命中,则直接从缓存中获取,**不与服务器发生请求**

  • Expire 是一个时间戳,浏览器会对比本地时间和 Expire 时间戳(如果改掉客户端本地时间, expire 将不能达到预期,这是 expire 的局限性)
    1
    expires: Wed, 11 Sep 2019 16:12:18 GMT
  • HTTP1.1新增了 Cache-Control 来替代 Expire 。(该字段是一个时间长度,避免了时间戳带来的问题。相比 expire,更准确,优先级更高,两个字段同时存在时,以 Cache-Control 为准)
    1
    cache-control: max-age=31536000

协商缓存由 Last-Modified 和 Etag 实现,两个同时存在时以 Etag 为准, 需要浏览器向服务器询问缓存信息,来决定是发起请求还是从本地缓存获取。

响应会返回 Last-Modified,请求会携带 If-Modified-Since

1
2
3
Last-Modified: Mon, 01 Jun 2020 06:34:26 GMT

If-Modified-Since: Mon, 01 Jun 2020 06:34:26 GMT

Last-Modified 常见问题:

  • If-Modified-Since 只能检查以秒为单位的时间差,修改时间过短会出问题
  • 如果修改了文件但是没改文件内容,会被当作新资源请求,所以出现了 Etag

Etag:基于文件内容,编码生成唯一标志字符串,避免了 Last-Modified 的两个问题。响应会返回 ETag,请求会携带 If-None-Match

1
2
3
ETag: W/"2a3b-1602480f459"

If-None-Match: W/"2a3b-1602480f459"
Memory Cache

内存缓存,优先级最高,速度快,进程结束后即被清空。常缓存 base64 格式的图片、体积较小的 js 和 css 文件,体积较大的会被存进磁盘。

Service Woker Cache

独立于主线程之外的 Javascript 线程。脱离于浏览器窗体,因此无法直接访问 DOM。用来实现离线缓存、消息推送和网络代理等功能,**仅支持 HTTPS 协议**

Push Cache

HTTP2 中的新知识

特点:

  • 在其他缓存均未命中的情况下,才会查找该缓存
  • 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放
  • 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

渲染优化

参考文章

推荐一本掘金小册:

前端性能优化原理与实践

常用的设计模式(然后说话你的项目中用到过的)

推荐一本掘金小册和一篇文章,然后结合自己的项目总结下:

Vuex的原理,怎么实现状态管理的

Vue双向绑定如何实现的,get和set进行了哪些操作

Vue生命周期

this.$nexttick原理

说下你了解的async和await?await的使用条件是什么?

前端请求的封装是如何做的?

遍历数组你常用的方法,有何区别

watch和computed相关用法以及区别

组件传参

深拷贝浅拷贝

做过公共组件抽离吗?

vue权限路由实现?如果后续路由层级加深,该如如何处理?

mixin在项目中有用到嘛?

你所做项目中觉得哪个是比较有的亮点的?是什么?

vue-router原理(怎么根据url找到我们对应的组件的)

数组去重

写一个数组排序方法(数组可能是简单数组,也可能是复杂数组)

项目打包后文件过大你是怎么优化的

页面加载时间过长你是怎么优化的

vue双向绑定原理

vue中computed与watch

介绍下协商缓存与强制缓存

项目中,关闭页面(或者浏览器后)未提交的数据如何保存与回填。你怎么确定存储的数据是这个页面的

js数据类型转换

js中类型转换有3种:

  • 转换为布尔型(调用
    boolean()方法)
  • 转换为数字(调用Number()、parseInt()和parseFloat()方法)
  • 转换为字符串(调用.toString()或者String()方法)

除以上三种外,还有一些操作符会存在隐式转换

  • 对象和布尔值比较:对象先转换成字符串然后转成数值,布尔值转成数值,然后两者进行比较
  • 对象和字符串比较:对象转成字符串,然后和字符串进行比较
  • 对象和数字比较:对象先转换成字符串然后转成数值,然后进行比较
  • 字符串和布尔值比较:两者先转成数值进行比较
  • 布尔值和数字比较:布尔值先转成数值进行比较

Number方法的参数是对象时

第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用 Number函数,不再进行后续步骤。(默认情况下,对象的valueOf方法返回对象本身)

第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。

第三步,如果toString方法返回的是对象,就报错。

1
2
3
4
5
6
7
8
9
var obj = {x: 1};
Number(obj) // NaN

// 等同于
if (typeof obj.valueOf() === 'object') {
Number(obj.toString());
} else {
Number(obj.valueOf());
}

js隐式转换原理

Vue 父子组件生命周期执行顺序

  1. 加载渲染过程
  • 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  1. 子组件更新过程
  • 父beforeUpdate->子beforeUpdate->子updated->父updated
  1. 父组件更新过程
  • 父beforeUpdate->父updated
  1. 销毁过程
  • 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

三栏布局

bfc

清除浮动

声明一个变量实现:a===1&&a===2&&a===3

方法一:

1
2
3
4
5
6
let val = 1
Object.defineProperty(this, 'a', {
get(){
return val++
}
})

方法二:利用 toString 方法

1
2
3
4
5
6
7
8
9
10
11
12
class A {
constructor(value) {
this.value = value;
}
toString() {
return this.value++;
}
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
console.log("success");
}

拓展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 设置一个函数输出以下的值
f(1) = 1;
f(1)(2) = 3;
f(1)(2)(3) = 6;

function f() {
let args = [...arguments];
let add = function() {
args.push(...arguments);
return add;
};
add.toString = function() {
return args.reduce((a, b) => {
return a + b;
});
};
return add;
}
console.log(f(1)(2)(3)); // 6

事件触发顺序

  • NetSpace:事件捕获先触发
  • MicroSoft: 事件冒泡先触发
  • W3C模型中:事件先捕获,在冒泡

js 基础数据类型与复杂数据类型的区别

7种基础数据类型:undefinednullnumberbooleanstringSymbol(ES6新增,表示独一无二的值)BigInt(ES10)

1种引用数据类型(复杂数据类型):Object,包含function、Array、Date等

  • 基本数据类型把数据名和值直接存储在栈当中占据空间小、大小固定,属于被频繁使用数据
  • 复杂数据类型在栈中存储数据名和一个在堆(占据空间大、大小不固定)中的地址在堆中存储属性及值,访问时先从栈中获取地址,再到堆中拿出相应的值

vue页面级的拆包

闭包及其作用

闭包就是能够读取其他函数内部变量的函数。闭包是将函数内部和函数外部连接起来的桥梁

如何从外部读取函数内部的局部变量?可通过闭包实现

  • 作用
      1. 封装私有变量
      1. 读取函数内部变量
      1. 变量始终保存在内存中(滥用闭包会造成内存泄漏)

原型、原型链

每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性

所有的普通的原型链最终都会指向Object.Prototype

箭头函数与普通函数的区别

  • 箭头函数是匿名函数,不能作为构造函数,不能使用new
  • 箭头函数不能绑定arguments,取而代之用rest参数…解决
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function A(a){
    console.log(arguments);
    }
    A(1,2,3,4,5,8);
    // [1, 2, 3, 4, 5, 8, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    let C = (...c) => {
    console.log(c);
    }
    C(3,82,32,11323);
  • 箭头函数没有原型属性
    1
    2
    let arrow = () => {}
    arrow.prototype // undefined
  • 箭头函数的this永远指向其上下文的this,没有办改变其指向,
    普通函数的this指向调用它的对象

vue中 v-if 和 v-for 同时出现,那个优先级高,同时存在时怎么优化性能?

  • v-for优先于v-if被解析
  • 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能
  • 优化一:要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环
    1
    2
    3
    <template v-if="...">
    <p v-for="item in itemArrValid" :key="item.id">{{item}}</p>
    </template>
  • 优化二:如果在循环内部进行if判断,我们可以先用computed过滤出有效的展示数据,再进行渲染
    1
    <p v-for="item in itemArrValid" :key="item.id">{{item}}</p>
    1
    2
    3
    4
    5
    6
    computed:{
    itemArrValid(){
    // return ...
    // 处理 itemArr 重的数据
    }
    }

v-for 绑定 key 的作用

v-for 的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key 来提供一个排序提示。———— 来源:Vue官网

key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的vnode节点。

  1. 更准确

    因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

  2. 更快

    利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。)

vue中的data

官方文档:https://cn.vuejs.org/v2/api/#data

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var data = { a: 1 }

// 直接创建一个实例
var vm = new Vue({
data: data
})
vm.a // => 1
vm.$data === data // => true

// Vue.extend() 中 data 必须是函数
var Component = Vue.extend({
data: function () {
return { a: 1 }
}
})

Vue 组件间传值

一、父子组件传值

  1. 父组件 传给 子组件
    • 父组件 通过 v-bind 绑定动态数据传给子组件
    • 子组件 通过 props 接收
    • 使用$ref获取指定的子组件
    • 使用$children
  2. 子组件 传给 父组件
    • 子组件 用 this.$emit('fnName', data) 触发 ``
    • 父组件 用 @fnName="fn" 接收,注意,@的名称 和子组件this.$emit名称相同
    • 使用 $parent

二、组件传值

  1. eventBus,利用 bus.$emit(‘eventName', sendData)触发,通过 bus.$on(‘eventName', (getData) => {})监听接收
  2. 使用Vuex
  3. $sttrs/listeners 用于多级组件间传值的问题
    1
    2
    3
    4
    // 一般在 main.js中引入
    Vue.prototype.bus=new Vue()
    // 或者
    window.bus = new Vue()

webpack 热更新原理

推荐文章:https://zhuanlan.zhihu.com/p/30669007

vue 组件封装

全局主题切换思路

vue 中怎么定义一个全局方法

方法一:

1
2
3
4
// 在 main.js 中
Vue.prototype.Fn = function () {
console.log(‘这是一个全局方法’)
}

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一个单独的js文件
// globalFn.js
const data = {
hello(param){
return param
}
}

const install = (vm, option) => {
vm.prototype.gFn = data
}
export default { install }

// main.js中引入

import gFn from './globalFn.js'
Vue.use(gFn)

// 使用

this.gFn.hello('hello')

如何保证API调用时数据的安全性?

  1. 通信使用https
  2. 请求签名,防止参数被篡改
  3. 身份确认机制,每次请求都要验证是否合法
  4. 对所有请求和响应都进行加解密操作

V8垃圾回收机制

下列代码输出什么

滑慢点,下面挨着的就是答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = 1;
let b = 2
let obj = {
a:11,
b:22,
fn: function(){
console.log(this.a, this.b)
},
fn1: () => {
console.log(this.a, this.b)
}
}
obj.fn()
obj.fn1()

解析:

本题个人看到有两个知识点:

1、this指向问题

2、let 声明的变量不会挂在window上,var 声明的会挂在 window 上

1
2
obj.fn() 输出: 1122
objfn1() 输出:1undefined