常见面试题
主要纪录面试过程中的面试题,对自己知识的查漏补缺
说一下深拷贝与浅拷贝以及他们的区别
浅拷贝
概念
概念: 对于字符串类型,浅拷贝是对值的复制,对于对象来说,浅拷贝是对对象地址的复制, 也就是拷贝的结果是两个对象指向同一个地址
方法
Object.assign或者(...)展开运算符
深拷贝
概念
概念: 深拷贝开辟一个新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
JSON.parse(JSON.stringify(object))或者递归
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
该方法也是有局限性:(1)会忽略 undefined(2)不能序列化函数(3)不能解决循环引用的对象
闭包
-
闭包就是能够读取其他函数内部变量的函数
-
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域
闭包的特性:
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
说说你对闭包的理解
使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念
-
闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
-
闭包的另一个用处,是封装对象的私有属性和私有方法
-
好处:能够实现封装和缓存等;
-
坏处:就是消耗内存、不正当使用会造成内存溢出的问题
使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
- 解决方法是,在退出函数之前,将不使用的局部变量全部删除
请描述一下 cookies,sessionStorage 和 localStorage 的区别?
-
cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)
-
cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递
-
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存
存储大小:
- cookie数据大小不能超过4k
- sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
有期时间:
- localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
- sessionStorage 数据在当前浏览器窗口关闭后自动删除
- cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
JS的基本数据类型和引用数据类型
- 基本数据类型(6种):undefined、null、boolean、number、string、symbol
- 引用数据类型(3种):object、array、function
说一下浏览器的缓存机制
浏览器缓存机制有两种,一种为强缓存,一种为协商缓存
-
对于强缓存,浏览器在第一次请求的时候,会直接下载资源,然后缓存在本地,第二次请求的时候,直接使用缓存。
-
对于协商缓存,第一次请求缓存且保存缓存标识与时间,重复请求向服务器发送缓存标识和最后缓存时间,服务端进行校验,如果失效则使用缓存
协商缓存相关设置
Exprires:服务端的响应头,第一次请求的时候,告诉客户端,该资源什么时候会过期。Exprires的缺陷是必须保证服务端时间和客户端时间严格同步。
Cache-control:max-age:表示该资源多少时间后过期,解决了客户端和服务端时间必须同步的问题,
If-None-Match/ETag:缓存标识,对比缓存时使用它来标识一个缓存,第一次请求的时候,服务端会返回该标识给客户端,客户端在第二次请求的时候会带上该标识与服务端进行对比并返回If-None-Match标识是否表示匹配。
Last-modified/If-Modified-Since:第一次请求的时候服务端返回Last-modified表明请求的资源上次的修改时间,第二次请求的时候客户端带上请求头If-Modified-Since,表示资源上次的修改时间,服务端拿到这两个字段进行对比
Vue面试题
请详细说下你对vue生命周期的理解
总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后,Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期
各个生命周期的作用
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activated | keep-alive专属,组件被激活时调用 |
deactivated | keep-alive专属,组件被销毁时调用 |
beforeDestroy | 组件销毁前调用 |
destroyed | 组件销毁后调用 |
Vue实现数据双向绑定的原理:Object.defineProperty()
-
vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue追踪依赖,在属性被访问和修改时通知变化。
-
vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。
Proxy 相比于 defineProperty 的优势
Object.defineProperty() 的问题主要有三个:
- 不能监听数组的变化
- 必须遍历对象的每个属性
- 必须深层遍历嵌套的对象
Proxy 在 ES2015 规范中被正式加入,它有以下几个特点
针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 第二个问题
支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。
除了上述两点之外,Proxy 还拥有以下优势:
Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。
v-for和v-if为什么不建议同时用
vue2中会优先执行 v-for, 当 v-for 把所有内容全部遍历之后 , v-if 再对已经遍历的元素进行删除 , 造成了加载的浪费 , 所以应该尽量在执行 v-for 之前优先执行 v-if , 可以减少加载的压力。(在vue3中v-if的优先级高于v-for)
解决方案:
(1)、外部条件放到遍历的父级元素上,没有父级可以使用。注意 key 不能放 template 标签上。
(2)、在计算属性中先用内/外部条件处理数据,再遍历处理后的数据
说说Vue2.0和Vue3.0有什么区别
- 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
可直接监听数组类型的数据变化
监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行
直接实现对象属性的新增/删除
- 新增Composition API,更好的逻辑复用和代码组织
- 重构 Virtual DOM
模板编译时的优化,将一些静态节点编译成常量
slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
- 代码结构调整,更便于Tree shaking,使得体积更小
- 使用Typescript替换Flow
介绍一下Vue中的Diff算法
在新老虚拟DOM对比时
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n^3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
15 说一说keep-alive实现原理
keep-alive组件接受三个属性参数:include、exclude、max
-
include 指定需要缓存的组件name集合,参数格式支持String, RegExp, Array。当为字符串的时候,多个组件名称以逗号隔开。
-
exclude 指定不需要缓存的组件name集合,参数格式和include一样。
-
max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。
原理 -
keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode
LRU(Least recently used) 算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)
关于宏任务/微任务,同步/异步的执行顺序的面试题
async function promise1() {
console.log("promise1 start")
await promise2()
console.log("promise1 end")
}
function promise2() {
console.log("promise2")
}
setTimeout(function () {
console.log("setTimeout")
}, 0)
console.log("script start")
promise1()
new Promise((resolve, reject) => {
console.log("Promise")
resolve()
}).then(function () {
console.log("Promise then")
})
console.log("script end")
宏任务
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
微任务
Promise.then
Object.observe(将要废弃)
MutaionObserver(新特性)
process.nextTick(Node.js 环境)
事件循环
- js是单线程,一个线程拥有唯一一个时间循环,但任务队列可以有多个。
- 任务队列又分为宏任务和微任务。
- 来自不同任务源的任务会进入到不同的任务队列。(setTimeout与setInterval是同源的)
- 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始进入第一次循环,代码一行一行执行,执行过程中遇到宏任务,把宏任务加到宏任务队列中, 遇到微任务放到微任务队列中,当宏任务的函数调用栈执全部执行后,去看有没有微任务, 如果有,去执行微任务, 微任务全部执行完成后,循环再次从宏任务开始,这样循环。
- 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程:宏任务->微任务->渲染->宏任务->微任务->渲染->...
promise, async/await
- promise是同步的,它里面的代码会同步执行。
- promise.then是微任务,promise.then里面的代码放到微任务队列中,等宏任务执行完成之后执行。
- async/await 是同步语法,解决异步回调问题,promise.then.catch 链式调用,但也是基于回调函数的。
- await会等待一个函数的执行结果,这个函数式同步的
- await下面的代码相当于promise.then也会放到微任务队列中。
揭晓答案
async function promise1() {
console.log("promise1 start")
await promise2()
console.log("promise1 end")
}
function promise2() {
console.log("promise2")
}
setTimeout(function () {
console.log("setTimeout")
}, 0)
console.log("script start")
promise1()
new Promise((resolve, reject) => {
console.log("Promise")
resolve()
}).then(function () {
console.log("Promise then")
})
console.log("script end")
------------------------------------
script start
promise1 start
promise2
Promise
script end
promise1 end
Promise then
setTimeout