杭州今夏前端2022面试真题,小伙伴们速来查收!
内容原创加部分总结前人,文字手敲,如有错误感谢各位指出 😊
浅拷贝
浅拷贝的对象一般是一个只有一层的对象或者数组,可以使用es6新增的语法Object.assign({},'被拷贝的对象')来实现。除此之外也可以用结构赋值的方式:比如{...obj}。
const obj1 = { a: 1, b: 2 }; // 定义一个对象
console.log(Object.assign({}, obj1)); // {a:1,b:2}
console.log({ ...obj1 }); // {a:1,b:2}
深拷贝
概念:深拷贝的对象一般是复杂数据类型套复杂数据类型的多层结构
第一种方法可以使用序列化和反序列化的方式来实现。代码体现为:JSON.stringify(obj)和JSON.parse(str)。 可实际上这种方式也是存在问题的。因为有一些数据类型会造成数据丢失和数据异常。例如:function、undefined 直接丢失;NaN、Infinity 和-Infinity 变成 null;RegExp、Error对象只得到空对象;
const obj2 = { a: 1, b: 2, c: { d: 3, e: 4 } };
const obj3 = JSON.parse(JSON.stringify(obj2));**
接下来是递归深拷贝。递归深拷贝需要注意一些问题。首先只要是递归就可能会出现堆栈溢出的问题,具体在下面例子实现。第二在不确定具体对象中存在哪些类型的数据时需要添加多层判断条件,本文以数组对象方法为例
const obj = {
name: "JOJO",
age: 24,
say: function () {
console.log("说话了");
},
obj1: {
name: "Luna",
age: 20,
arr1: [1, 2, 3, 4, 5],
},
arr: [1, 2, 3],
};
function deepClone(obj) {
let newArray = Array.isArray(obj) ? [] : {}; // 进判断前首先类型检查,数组就转成数组,对象就改为对象
for (let k in obj) {
if (typeof obj[k] === "object") {
if (obj[k] === obj) {
break; // 如果此处进来的数据是自身,就跳出循环,否则会出现内存溢出。这里需要注意的是,不能使用return而是应该用break,因为return会跳出函数不再执行后面正常数据的深拷贝,而break只跳出本次判断。
}
newArray[k] = deepClone(obj[k]);
} else {
newArray[k] = obj[k];
}
}
return newArray;
}
const newObj = deepClone(obj);
console.log(newObj);
newObj.obj1.name = "小黄";
console.log(obj);
字符串方法的使用
substr和substring的区别
str = "abcde";
console.log(str.substr(1, 3)); // bcd
console.log(str.substring(1, 3)); //bc
通过这段简单的代码可以得到结论 --- substr(参数1,参数2),subString(参数1,参数2)
substr()第一个参数是起始位置,第二个参数是从当前位置开始要截取的字符串长度
subString()方法返回的字符串包括开始处的字符,但不包括结束处的字符。
slice和splice的区别
const arr1 = [1, 2, 3, 4, 5];
console.log(arr1.slice(1, 3)); // [2,3]
console.log(arr1.slice(3)); // [4,5]
console.log(arr1); // [1,2,3,4,5]
由此可见,slice方法是拷贝数组的一部分,而不是改变原数组。 第一个参数是起始位置,第二个参数是结束位置,如果不传第二个参数,则默认为数组的最后一个位置。 起始位置如果是负数则表示从后往前,如果省略起始下标则默认是从0开始
console.log(arr1.splice(1, 3, 5)); // [2,3,4]
console.log(arr1); // [1,5,5]
由此可见,splice方法是改变数组的一部分,而不是拷贝数组。 第一个参数是起始位置,第二个参数是删除的个数,第三个参数是插入的元素,如果不传第三个参数,则默认删除指定个数的元素。
Vue2状态管理工具Vuex
Vue2的状态管理工具是Vuex。vuex的作用同样是提供了一个集中的数据存储库,对多个页面组件都要使用的数据进行管理和修改。vuex使用state函数初始化数据,mutations函数提供了同步修改数据的方法,actions提供了异步修改数据。getters函数返回的是一个state中数据的映射,有点类似于map。actions修改数据实际上是通过调用mtations中的方法实现的,可以解释为所有的数据修改都要从mutations中进行。因为如果在外部修改state中的数据一旦报错,将无法精准定位错误信息。而如果仓库的管理方式是雇佣一个人,那么只要出现问题就可以只找他。
Vue3状态管理工具Pinia
首先简单介绍一下pinia菠萝: pinia是Vue3的状态管理工具,至于为什么叫这个名字,据尤大大说是因为尊重原作者~
其次,在vue3中依然支持vuex作为状态管理工具,那么pinia的优点究竟在哪里,值不值得去学习呢?
1.pinia移除了大家饱受诟病的mutations,减少了工作量。 2.pinia支持大量插件,vuex是不支持的 3.vue3使用ts编写,而pinia有着对ts强大的支持 4.vuex中如果产生大量数据则需要创建多个模块防止代码冗余,pinia不需要,因为本身每一个store都是独立的,互相不影响。 5.pinia的体积很小,大概只有1k
以上就是pinia的使用优点
事件循环—宏任务和微任务
首先我们要了解,js是一个单线程任务,那就代表了在一段时间内只能执行同一件事情,后面的事情就只能等待前面的事情完成再执行。
那么为了解决这个问题js会委托宿主环境(浏览器)帮忙执行这些耗时任务,再执行完成后通知js去执行回调,那么宿主环境执行的任务就是异步任务。
还需要再知道的一件事是,在es5之前,js无法发起异步任务,es5之后新增了js内置对象promise才可以进行异步操作,结合图片更容易理解。
再来说说事件循环eventloop:
js的执行机制是,主线程会先判断任务是同步任务还是异步任务,同步任务由主线程自己执行,异步任务交给浏览器执行。异步任务在执行完毕后将回调放进任务队列中,按照执行完成的顺序依次放入,等到同步任务全部执行完成,任务队列中的异步任务就会开始执行,由于异步任务完成有先后顺序,执行过程也是遵循"先进先出"的原则。消息队列会不断的查找有没有新的回调,有就执行,这样的流程就是事件循环。
宏任务与微任务:
宏任务:由浏览器创建并执行的任务,一般有定时器和一些特殊的代码块组成。
微任务:由js自身发起的异步任务---注意是由js内置对象自己发起的!!
执行顺序: 1. 率先执行宏任务。 2. 如果当前宏任务下有微任务,执行完宏任务后会把所有微任务全部执行。 3. 执行完所有微任务后,执行下一个宏任务。
// 提供几个小题目供大家思考,答案就不放了,大家可以cv去编辑器输出看结果是否和你想的一致。
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
new Promise(function(resolve) {
console.log(3)
resolve()
}).then(function() {
console.log(4)
})
console.log(5)
-------------------------------------------
async function async1() {
console.log('async1 start')
await async2()
// await后面的代码可以理解为promise.then(function(){ 回调执行的 })
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
console.log('script end')
vue数据双向绑定原理
数据驱动视图
视图修改数据
Vue中的指令都可以实现数据变了,视图更新。但是有一个指令v-model一般用于表单控件,可以实现数据双向绑定,所谓的双向绑定就是视图变了,数据也会跟着变。v-model是一个语法糖,实现原理其实是用v-on绑定input事件配合插值表达
式实现数据双向绑定
vue2和vue3的数据响应原理
vue2的数据响应式原理事实上是通过数据劫持Object.defineProperty(),和观察者模式实现的。首先vue2中的响应式数据都必须先在data中初始化,实际上就是先用Object.keys获取一个带有data数据键名的数组,再对数组遍历,并对data中的每一个需要响应的数据添加get()set()方法。这样vue内部就可以知道数据是否发生改变。每一个响应式数据都会创建一个dep类,编译模板获取所有依赖dep的watcher并添加到对应的观察者列表。在捕获数据的更新时,通过dep类通知所有模板中依赖dep类的watcher,使用set方法更新数据。实现数据的响应式。而数据一般会存在复杂数据类型嵌套复杂数据类型的情况,这时则需要使用递归算法,每一层都创建一个dep类,实现层层捕获更新以及通知。尤大大在提问中曾解释过,为什么数组下标实际上也是可以被数据劫持的,但是为什么没有用这种方式实现响应式。这个问题其实原因很简单,在性能测试中,用splice和this.$set()方法和数组下标处理响应式的时候,前1*10^6条数据时的性能差别不大都在50毫秒左右,但是超过这个数字后,使用捕获的方法耗时超过450毫秒,几乎是另外一种方法的9倍,因此废弃了这种方法。至于后添加的数据为什么不是响应式也很简单,因为后添加的数据错过了响应式初始化的过程。
vue3的响应式原理
浏览器的解析机制和优化
浏览器会把html解析成一颗dom树,节点是html标签,然后会解析css样式,并生成一颗cssom规则树。然后把这两个混合以后生成一个render tree 渲染树,按照渲染树来绘制页面。
浏览器第一次绘制页面会引发两种解析现象。回流和重绘。回流是指页面的结构布局和大小发生改变。重绘指的是元素的颜色或者背景颜色发生改变。回流一定会引起重绘,重回不一定会引起回流。
优化:前端优化的方式一般是:
1.把内容先用dispaly属性设置为none,然后再做完想要修改的操作以后再把他显示,这样的话只会产生两次回流与重绘。
2.浏览器绘制页面的时候本身是一个异步操作,会把绘制任务放进异步队列中。
3.把需要频繁变更样式的部分开启bfc模式,这样就可以不影响外部布局。
4.在做dom操作之前先用document.createDocumentFragment()方法创建一个代码片段,然后在代码片段中进行相应操作最后再添加到dom树中,这样可以实现只发生一次回流和重绘。
5.如果是操作一些可复用的节点元素,使用clone方法可以避免操作dom。
6.尽量不要写行内样式,最好直接修改className
defer和sync的区别
浏览器解析html页面,遇到外联js脚本,如果没有异步修饰符就会直接终端页面渲染并下载脚本然后执行,这样会造成页面不流畅。因此有两种异步修饰符用于脚本加载。
defer修饰符:当浏览器解析遇到带有defer修饰符的js脚本就会创建一个异步任务开始下载脚本,同时继续下载渲染dom,等到所有的内容渲染完成再执行js。
sync修饰符:当浏览器遇到带有defer修饰符的js脚本同样会创建一个异步任务开始下载,但是这个js只要下载完成就会中断dom渲染,直接执行,等到执行完毕再进行渲染。并且有多个sync修饰的js脚本会按照先后下载完成顺序执行,不能保证执行顺序。
总结:如果js脚本之间不存在关联性,可以使用sync修饰符。如果存在关联性,选择保险起见的defer修饰符
虚拟DOM和DIFF算法
虚拟dom 的本质就是一个对象,包含了每个节点的属性,是一个真实dom的映射。为什么要使用虚拟dom?因为:操作真实dom的代价很大,会引起回流和重绘,每一个标签身上都有很多的属性,如果只是为了修改一个属性而重新渲染整个节点会造成极大的性能浪费。操作虚拟dom只会在全部更新完成后执行一次重绘回流。创建虚拟dom是在beforeMount阶段
vue怎么比较虚拟dom和真实dom:
diff算法:diff算法按照广度优先,同层比较原则。广度优先就是如果我的根节点发生了改变,内部直接不考虑,重新绘制。这样查找的优点在于时间复杂度o(n)是线性的,虽然查找速度快,但是节点内部可复用的部分也失去了。如果节点只改变属性,那就只修改真实dom的属性。diff算法能很好的解释为什么v-for需要写:key。循环的节点被数组方法打乱顺序时,就需要key值对应新旧元素,避免多于更新。虚拟dom的最大作用是跨平台。
怎么实现前端的权限控制
前端实现权限控制一般是使用动态路由完成。初始要设置两个路由表,一个是静态路由表,一个是动态路由表。静态路由表存放的是所有人都可以访问的路由。动态路由放的是具有权限信息的路由表。路由权限大概可以分为三个小部分。
- 首先是路由信息不同,具体是后台项目中,某一些页面员工不能访问,然后管理员可以访问。然后如果员工访问没有权限的页面就会跳转404或者显示当前没有权限。具体实现为,静态路由默认所有人可见,根据后端返回的权限信息使用Vue提供的api-addRoutes实现动态添加路由表完成。
- 一般来说中后台项目的导航栏也是具有权限访问的,具体实现为,把用户的静态路由和动态路由合并,然后遍历这个路由表来实现不同权限展示不同菜单。
- 页面中一般会存在一些权限按钮或者其他的api。具体实现大致可以分为两种。一种是采用混入,后端一般会返回一个权限点,根据这个权限点,定义好函数并全局引入。在需要设置权限的按钮上可以使用disabled属性或者是display属性显示隐藏达到权限控制。另外一种就是采用过滤器了。但在vue3版本下都会采用组合式api来实现!
虚拟dom和diff算法
这个是一个老生常谈的问题,我简单概括一下概念。虚拟dom首次提出是由Facebook,最早是在react框架应用。vue2版本也引入了虚拟dom。虚拟dom本杂志上是一个描述真实dom的js对象,是真实dom的完整映射。为什么要用虚拟dom呢?
这里引用前人的一句经典台词。虚拟dom就像是两座小岛,中间只有一座窄桥可供通过,每一次人们过桥办事,都要付给守桥的人高昂的过路费,因此人们需要在越少的次数中完成更多的事情。真实dom有许许多多的节点,每一个节点都包含了更多的属性,如果只是为了修改其中的一个属性而引起一次重绘回流那将是极大的性能浪费。这就是我们使用虚拟dom的原因。页面初始化的时候会渲染一棵真实dom和一棵虚拟dom,在我们的页面发生变化时,会先产生一棵新的虚拟dom,然后使用diff算法进行比对,等到所有的操作结束以后,再根据最后的虚拟dom修改真实dom。只发生一次重排
再浅聊一下diff算法:
diff算法顾名思义是一种查找机制,计算机专业的小伙伴应该都学过数据结构,当时我学的时候很痛苦~
diff算法是一个线性查找,时间复杂度是o(n)具体来说就是,广度优先,同级比较。每一个dom都是由一个个Vnode组成的,diff算法会比较被修改位置的根节点,如果根节点发生改变,内容不看直接删除重绘,这就有一个缺点就是,里面可复用的内容也一并删掉了。如果是某一个属性修改则是仅修改当前属性。以vue为例,v-for遍历生成一个ul和li,如果不加key属性,修改某一个li会按照index来比较先后两次的结构,但是如果遇到一些数组方法会改变先后顺序,那么就会引起多余的更新或者修改操作。加上key属性,可以告诉vue每一个li先后对应情况,最高效的修改。