2022秋招前端面试题(六)(附答案)

代码输出结果

console.log(1); setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3) }); }); new Promise((resolve, reject) => { console.log(4) resolve(5) }).then((data) => { console.log(data); })setTimeout(() => { console.log(6); })console.log(7); 复制代码

代码输出结果如下:
1 4 7 5 2 3 6 复制代码

代码执行过程如下:
  1. 首先执行scrip代码,打印出1;
  2. 遇到第一个定时器setTimeout,将其加入到宏任务队列;
  3. 遇到Promise,执行里面的同步代码,打印出4,遇到resolve,将其加入到微任务队列;
  4. 遇到第二个定时器setTimeout,将其加入到红任务队列;
  5. 执行script代码,打印出7,至此第一轮执行完成;
  6. 指定微任务队列中的代码,打印出resolve的结果:5;
  7. 执行宏任务中的第一个定时器setTimeout,首先打印出2,然后遇到 Promise.resolve().then(),将其加入到微任务队列;
  8. 执行完这个宏任务,就开始执行微任务队列,打印出3;
  9. 继续执行宏任务队列中的第二个定时器,打印出6。
事件流
事件流是网页元素接收事件的顺序,"DOM2级事件"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是时间冒泡阶段,可以在这个阶段对事件做出响应。
虽然捕获阶段在规范中规定不允许响应事件,但是实际上还是会执行,所以有两次机会获取到目标对象。
事件冒泡我是父元素我是子元素
复制代码

当容器元素及嵌套元素,即在捕获阶段又在冒泡阶段调用事件处理程序时:事件按DOM事件流的顺序执行事件处理程序:
  • 父级捕获
  • 子级捕获
  • 子级冒泡
  • 父级冒泡
且当事件处于目标阶段时,事件调用顺序决定于绑定事件的书写顺序,按上面的例子为,先调用冒泡阶段的事件处理程序,再调用捕获阶段的事件处理程序。依次alert出“子集冒泡”,“子集捕获”。
为什么需要浏览器缓存?
对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。
所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
使用浏览器缓存,有以下优点:
  • 减少了服务器的负担,提高了网站的性能
  • 加快了客户端网页的加载速度
  • 减少了多余网络数据传输
事件触发的过程是怎样的
事件触发有三个阶段:
  • window 往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
// 以下会先打印冒泡然后是捕获 node.addEventListener( 'click', event => { console.log('冒泡') }, false ) node.addEventListener( 'click', event => { console.log('捕获 ') }, true ) 复制代码

通常使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
  • capture:布尔值,和 useCapture 作用一样
  • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive:布尔值,表示永远不会调用 preventDefault
一般来说,如果只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。
stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener( 'click', event => { event.stopImmediatePropagation() console.log('冒泡') }, false ) // 点击 node 只会执行上面的函数,该函数不会执行 node.addEventListener( 'click', event => { console.log('捕获 ') }, true ) 复制代码

如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象
浏览器本地存储方式及使用场景
(1)Cookie Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie。
Cookie的特性:
  • Cookie一旦创建成功,名称就无法修改
  • Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的,这也是由Cookie的隐私安全性决定的,这样就能够阻止非法获取其他网站的Cookie
  • 每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
  • 有安全问题,如果Cookie被拦截了,那就可获得session的所有信息,即使加密也于事无补,无需知道cookie的意义,只要转发cookie就能达到目的
  • Cookie在请求一个新的页面的时候都会被发送过去
如果需要域名之间跨域共享Cookie,有两种方法:
  1. 使用Nginx反向代理
  2. 在一个站点登陆之后,往其他网站写Cookie。服务端的Session存储到一个节点,Cookie存储sessionId
Cookie的使用场景:
  • 最常见的使用场景就是Cookie和session结合使用,我们将sessionId存储到Cookie中,每次发请求都会携带这个sessionId,这样服务端就知道是谁发起的请求,从而响应相应的信息。
  • 可以用来统计页面的点击次数
(2)LocalStorage LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。
LocalStorage的优点:
  • 在大小方面,LocalStorage的大小一般为5MB,可以储存更多的信息
  • LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
  • 仅储存在本地,不像Cookie那样每次HTTP请求都会被携带
LocalStorage的缺点:
  • 存在浏览器兼容问题,IE8以下版本的浏览器不支持
  • 如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
  • LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问
LocalStorage的常用API:
// 保存数据到 localStorage localStorage.setItem('key', 'value'); // 从 localStorage 获取数据 let data = https://www.it610.com/article/localStorage.getItem('key'); // 从 localStorage 删除保存的数据 localStorage.removeItem('key'); // 从 localStorage 删除所有保存的数据 localStorage.clear(); // 获取某个索引的Key localStorage.key(index) 复制代码

LocalStorage的使用场景:
  • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的LocalStorage中,当需要换肤的时候,直接操作LocalStorage即可
  • 在网站中的用户浏览信息也会存储在LocalStorage中,还有网站的一些不常变动的个人信息等也可以存储在本地的LocalStorage中
(3)SessionStorage SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
SessionStorage与LocalStorage对比:
  • SessionStorage和LocalStorage都在本地进行数据存储;
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享;
  • LocalStorage和SessionStorage都不能被爬虫爬取;
SessionStorage的常用API:
// 保存数据到 sessionStorage sessionStorage.setItem('key', 'value'); // 从 sessionStorage 获取数据 let data = https://www.it610.com/article/sessionStorage.getItem('key'); // 从 sessionStorage 删除保存的数据 sessionStorage.removeItem('key'); // 从 sessionStorage 删除所有保存的数据 sessionStorage.clear(); // 获取某个索引的Key sessionStorage.key(index) 复制代码

SessionStorage的使用场景
  • 由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。
实现数组原型方法
forEach
语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
参数:
callback:为数组中每个元素执行的函数,该函数接受1-3个参数currentValue: 数组中正在处理的当前元素index(可选): 数组中正在处理的当前元素的索引array(可选): forEach() 方法正在操作的数组 thisArg(可选): 当执行回调函数 callback 时,用作 this 的值。
返回值:undefined
Array.prototype.forEach1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } // 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。 const O = Object(this); // O.length >>> 0 无符号右移 0 位 // 意义:为了保证转换后的值为正整数。 // 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型 const len = O.length >>> 0; let k = 0; while(k < len) { if(k in O) { callback.call(thisArg, O[k], k, O); } k++; } } 复制代码

map
语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])
参数:与 forEach() 方法一样
返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
Array.prototype.map1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let newArr = []; // 返回的新数组 let k = 0; while(k < len) { if(k in O) { newArr[k] = callback.call(thisArg, O[k], k, O); } k++; } return newArr; } 复制代码

filter
语法:arr.filter(callback(element [, index [, array]])[, thisArg])
参数:
callback: 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg(可选): 执行 callback 时,用于 this 的值。
返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。
Array.prototype.filter1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let newArr = []; // 返回的新数组 let k = 0; while(k < len) { if(k in O) { if(callback.call(thisArg, O[k], k, O)) { newArr.push(O[k]); } } k++; } return newArr; } 复制代码

some
语法:arr.some(callback(element [, index [, array]])[, thisArg])
参数:
callback: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg(可选): 执行 callback 时,用于 this 的值。
返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false。
Array.prototype.some1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; while(k < len) { if(k in O) { if(callback.call(thisArg, O[k], k, O)) { return true } } k++; } return false; } 复制代码

reduce
语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])
参数:
callback: 一个 “reducer” 函数,包含四个参数:
preVal:上一次调用 callback 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]
curVal:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]
curIndex(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。
array(可选):用于遍历的数组。
initialValue(可选): 作为第一次调用 callback 函数时参数 preVal 的值。若指定了初始值 initialValue,则 curVal 则将使用数组第一个元素;否则 preVal 将使用数组第一个元素,而 curVal 将使用数组第二个元素。
返回值:使用 “reducer” 回调函数遍历整个数组后的结果。
Array.prototype.reduce1 = function(callback, initialValue) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; let accumulator = initialValue; // 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值 if(accumulator === undefined) { while(k < len && !(k in O)) { k++; } // 如果超出数组界限还没有找到累加器的初始值,则TypeError if(k >= len) { throw new TypeError('Reduce of empty array with no initial value'); } accumulator = O[k++]; } while(k < len) { if(k in O) { accumulator = callback(accumulator, O[k], k, O); } k++; } return accumulator; } 复制代码

对浏览器的理解
浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。
  • shell 是指浏览器的外壳:例如菜单,工具栏等。主要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种功能的。
  • 内核是浏览器的核心。内核是基于标记语言显示内容的程序或模块。
事件总线(发布订阅模式)
class EventEmitter { constructor() { this.cache = {} } on(name, fn) { if (this.cache[name]) { this.cache[name].push(fn) } else { this.cache[name] = [fn] } } off(name, fn) { let tasks = this.cache[name] if (tasks) { const index = tasks.findIndex(f => f === fn || f.callback === fn) if (index >= 0) { tasks.splice(index, 1) } } } emit(name, once = false, ...args) { if (this.cache[name]) { // 创建副本,如果回调函数内继续注册相同事件,会造成死循环 let tasks = this.cache[name].slice() for (let fn of tasks) { fn(...args) } if (once) { delete this.cache[name] } } } }// 测试 let eventBus = new EventEmitter() let fn1 = function(name, age) { console.log(`${name} ${age}`) } let fn2 = function(name, age) { console.log(`hello, ${name} ${age}`) } eventBus.on('aaa', fn1) eventBus.on('aaa', fn2) eventBus.emit('aaa', false, '布兰', 12) // '布兰 12' // 'hello, 布兰 12' 复制代码

数据类型判断 typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。
function typeOf(obj) { -let res = Object.prototype.toString.call(obj).split(' ')[1] -res = res.substring(0, res.length - 1).toLowerCase() -return res // 更好的写法 +return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() } typeOf([])// 'array' typeOf({})// 'object' typeOf(new Date)// 'date' 复制代码

代码输出结果
function SuperType(){ this.property = true; }SuperType.prototype.getSuperValue = https://www.it610.com/article/function(){ return this.property; }; function SubType(){ this.subproperty = false; }SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function (){ return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); 复制代码

输出结果:true
实际上,这段代码就是在实现原型链继承,SubType继承了SuperType,本质是重写了SubType的原型对象,代之以一个新类型的实例。SubType的原型被重写了,所以instance.constructor指向的是SuperType。具体如下:
实现节流函数和防抖函数
函数防抖的实现:
function debounce(fn, wait) { var timer = null; return function() { var context = this, args = [...arguments]; // 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; }// 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; } 复制代码

函数节流的实现:
// 时间戳版 function throttle(fn, delay) { var preTime = Date.now(); return function() { var context = this, args = [...arguments], nowTime = Date.now(); // 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } }; }// 定时器版 function throttle (fun, wait){ let timeout = null return function(){ let context = this let args = [...arguments] if(!timeout){ timeout = setTimeout(() => { fun.apply(context, args) timeout = null }, wait) } } } 复制代码

网络劫持有哪几种,如何防范?
?络劫持分为两种:
(1)DNS劫持: (输?京东被强制跳转到淘宝这就属于dns劫持)
  • DNS强制解析: 通过修改运营商的本地DNS记录,来引导?户流量到缓存服务器
  • 302跳转的?式: 通过监控?络出?的流量,分析判断哪些内容是可以进?劫持处理的,再对劫持的内存发起302跳转的回复,引导?户获取内容
(2)HTTP劫持: (访问?歌但是?直有贪玩蓝?的?告),由于http明?传输,运营商会修改你的http响应内容(即加?告)
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,?http劫持依然?常盛?,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商?法获取明?,就?法劫持你的响应内容。
如何减少 Webpack 打包体积
(1)按需加载 在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。
按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。
(2)Scope Hoisting Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如希望打包两个文件:
// test.js export const a = 1 // index.js import { a } from './test.js' 复制代码

对于这种情况,打包出来的代码会类似这样:
[ /* 0 */ function (module, exports, require) { //... }, /* 1 */ function (module, exports, require) { //... } ] 复制代码

但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:
[ /* 0 */ function (module, exports, require) { //... } ] 复制代码

这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:
module.exports = { optimization: { concatenateModules: true } } 复制代码

(3)Tree Shaking Tree Shaking 可以实现删除项目中未被引用的代码,比如:
// test.js export const a = 1 export const b = 2 // index.js import { a } from './test.js' 复制代码

对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。
如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
怎么加事件监听,两种
onclick 和 addEventListener
事件传播机制(事件流)
冒泡和捕获
事件循环机制 (Event Loop)
事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
先执行 Script 脚本,然后清空微任务队列,然后开始下一轮事件循环,继续先执行宏任务,再清空微任务队列,如此往复。
  • 宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
  • 微任务:process.nextTick()/Promise
上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。
【2022秋招前端面试题(六)(附答案)】优先级
  • setTimeout = setInterval 一个队列
  • setTimeout > setImmediate
  • process.nextTick > Promise
for (const macroTask of macroTaskQueue) { handleMacroTask(); for (const microTask of microTaskQueue) { handleMicroTask(microTask); } } 复制代码

Vue路由守卫有哪些,怎么设置,使用场景等
常用的两个路由守卫:router.beforeEach 和 router.afterEach每个守卫方法接收三个参数:to: Route: 即将要进入的目标 路由对象from: Route: 当前导航正要离开的路由next: Function: 一定要调用该方法来 resolve 这个钩子。在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。 判断是否登录,是否拿到对应的路由权限等等。复制代码

    推荐阅读