【面试题】「2023」JavaScript 最新高频 前端面试题 指南 (必看)(一):https://developer.aliyun.com/article/1414026
63. 说一说Cookie、SessionStorage、LocalStorage三者的区别?
首先,Cookie、SessionStorage、 LocalStorage都是浏览器的本地存储。它们之前的区别可以从以下几个方面来说:
- 写入方式:cookie是由服务器端写入的,而SessionStorage、 LocalStorage都是由前端写入的
- 生命周期:cookie的生命周期是由服务器端在写入的时候就设置好的,LocalStorage是写入就一直存在,除非手动清除,SessionStorage是页面关闭的时候就会自动清除。
- 存储大小:cookie的存储空间比较小大概4KB,SessionStorage、 LocalStorage存储空间比较大,大概5M。
- 数据共享:三者数据共享都遵循同源原则,SessionStorage还限制必须是同一个页面。
- 发送请求是否携带:在前端给后端发送请求的时候会自动携带Cookie中的数据,但是SessionStorage、 LocalStorage不会。
- 应用场景:Cookie一般用于存储登录验证信息SessionID或者token,LocalStorage常用于存储不易变动的数据,减轻服务器的压力,SessionStorage可以用来检测用户是否是刷新进入页面,如音乐播放器恢复播放进度条的功能。
扩展:从安全性来说,因为每次http请求都会携带cookie信息,这样无形中浪费了带宽,所以cookie应该尽可能少的使用,另外cookie还需要指定作用域,不可以跨域调用,限制比较多。但是用来识别用户登录来说,cookie还是比storage更好用的。其他情况下,尽量使用storage。
storage在存储数据的大小上面秒杀了cookie,现在基本上很少使用cookie了。
localStorage和sessionStorage唯一的差别一个是永久保存在浏览器里面,一个是关闭网页就清除了信息。localStorage可以用来跨页面传递参数,sessionStorage用来保存一些临时的数据,防止用户刷新页面之后丢失了一些参数。
64. js中数组是如何在内存中存储的?
数组不是以一组连续的区域存储在内存中,而是一种哈希映射的形式。它可以通过多种数据结构来实现,其中一种是链表。
js分为基本类型和引用类型:
- 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问;
- 引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
65. 如何检测浏览器版本?
检测浏览器版本一共有两种方式:
- 一种是检测
window.navigator.userAgent
的值,但这种方式很不可靠,因为userAgent
可以被改写,并且早期的浏览器如 ie,会通过伪装自己的 userAgent 的值为 Mozilla 来躲过服务器的检测。 - 第二种方式是功能检测,根据每个浏览器独有的特性来进行判断,如 ie 下独有的
ActiveXObject
。
66. JavaScript中的错误有哪些类型?
- Error:
Error
是最基本的错误类型,其他的错误类型都继承自该类型。因此,所有错误的类型共享了一组相同的属性。 这个类型的错误很少见。一般使用开发人员自定义抛出的错误。 - EvalError:这个错误会在使用
eval()
函数发生异常时候抛出。 - RangeError:在数值超出相应范围时触发。
- ReferenceError:一般出现在变量找不到的情况时触发。
- SyntaxError:当Javascript语言解析代码时,Javascript引擎发现了不符合语法规范的tokens或token顺序时抛出SyntaxError。
- TypeError:这个错误在JavaScript中是经常遇到的,不管是初学者还是老手。在变量中保存着以外的类型时,或者在访问不存在的方法时。都会导致这种错误。但是归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。
- URIError:在使用encodeURI或者decodeURI因为URL格式不正确时,就会导致URIError错误。这种错误也很少见。
67. ajax、fetch、axios有什么区别?
(1) AJAX
Ajax
即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:
- 本身是针对MVC编程,不符合前端MVVM的浪潮
- 基于原生XHR开发,XHR本身的架构不清晰
- 不符合关注分离(Separation of Concerns)的原则
- 配置和调用方式非常混乱,而且基于事件的异步模型不友好。
(2)Fetch
fetch
号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持 async/await
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式
fetch的缺点:
- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项:fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
(3)Axios
Axios
是一种基于Promise封装的HTTP客户端,其特点如下:
- 浏览器端发起XMLHttpRequests请求
- node端发起http请求
- 支持Promise API
- 监听请求和返回
- 对请求和返回进行转化
- 取消请求
- 自动转换json数据
- 客户端支持抵御XSRF攻击
更多题库 地址:前端面试题库
68. for...in 和 for...of 有什么区别?
for…of
是ES6新增的遍历方式,允许遍历一个含有iterator
接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in
的区别如下:
for…of
遍历获取的是对象的键值,for…in 获取的是对象的键名;for…in
会遍历对象的整个原型链,性能非常差不推荐使用,而 for…of 只遍历当前对象不会遍历原型链;- 对于数组的遍历,
for…in
会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of
只返回数组的下标对应的属性值;
总结:for...in
循环主要是为了遍历对象而生,不适用于遍历数组;for...of
循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
69. 如果new一个箭头函数会怎么样?
会直接报错。箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
new操作符的实现步骤如下:
1、创建一个空的简单JavaScript对象(即{});
2、为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
3、将步骤1新创建的对象作为this的上下文 ;
4、如果该函数没有返回对象,则返回this。
所以,上面的第二、三步,箭头函数都是没有办法执行的。
70. 检测数据类型的方式有哪些?
1. typeof:其中数组、对象、null都会被判断为object,其他判断都正确。
console.log(typeof 1); // number console.log(typeof true); // boolean console.log(typeof 'CoderBin'); // string console.log(typeof [1,2]); // object console.log(typeof function(){}); // function console.log(typeof {}); // object console.log(typeof undefined); // undefined console.log(typeof null); // object 复制代码
2. instanceof:可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(1 instanceof Number); // false console.log(true instanceof Boolean); // false console.log('CoderBin' instanceof String); // false console.log([1,2,3] instanceof Array); // true console.log(function(){} instanceof Function); // true console.log({} instanceof Object); // true 复制代码
可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
3. constructor
console.log((1).constructor === Number); // true console.log((true).constructor === Boolean); // true console.log(('CoderBin').constructor === String); // true console.log(([1,2]).constructor === Array); // true console.log((function() {}).constructor === Function); // true console.log(({}).constructor === Object); // true 复制代码
4. Object.prototype.toString.call(): 使用 Object 对象的原型方法 toString
来判断数据类型:
var test = Object.prototype.toString; console.log(test.call(1)); console.log(test.call(true)); console.log(test.call('CoderBin')); console.log(test.call([1,2,3])); console.log(test.call(function(){})); console.log(test.call({})); console.log(test.call(undefined)); console.log(test.call(null)); // 结果如下: // [object Number] // [object Boolean] // [object String] // [object Array] // [object Function] // [object Object] // [object Undefined] // [object Null] 复制代码
71. Object.is() 与比较操作符"==="、"=="的区别?
- 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
- 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
- 使用
Object.is
来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
72. isNaN 和 Number.isNaN 函数有什么区别?
- 函数
isNaN
接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响NaN
的判断。 - 函数
Number.isNaN
会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
总结: 和全局函数 isNaN()
相比,Number.isNaN()
不会自行将参数转换成数字,只有在参数是值为 NaN 的数字时,才会返回 true。
Number.isNaN()
方法确定传递的值是否为NaN,并且检查其类型是否为Number。它是原来的全局isNaN()
的更稳妥的版本。
73. 谈谈你对浏览器中进程和线程的理解
进程和线程(一个进程中可以有多个线程)
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
进程之间互相独立。线程是不能单独存在的,它是由进程来启动和管理的。
进程和线程的关系
- 进程中任意一个线程执行出错,都会导致整个进程的崩溃
- 线程之间共享进程中的数据
- 当一个进程关闭之后,操作系统会回收进程所占用的内存
- 进程之间的内容相互隔离
- 一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,就需要使用用于进程间通信(IPC)的机制
浏览器是多进程的
一般每打开一个Tab页,就相当于创建了一个独立的浏览器进程。但有时会进行进程合并。
更多题库 地址:前端面试题库
75. JS代码中的"use strict"是什么意思?
use strict
是一种ECMAscript5添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。
设立"严格模式"的目的,主要有以下几个:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript 做好铺垫。
区别:
- 禁止使用with语句。
- 禁止this关键字指向全局对象。
- 对象不能有重名的属性。
76. Service worker是什么?
Srvice worker
是 PWA 的重要组成部分,W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API,主要用来做持久的离线缓存,也是Web Worker的升级版。
Service worker
(简称 SW) 是一个注册在指定源和路径下的事件驱动 Worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
77. 如何判断一个对象是不是空对象?
- 使用
Object.keys()
将对象属性名组成数组,如果length
为0
表示空对象 - 使用
JSON.stringify()
将对象转成字符串,如果为{}
则表示空对象
// 方法 1 Object.keys(obj).length === 0 // 方法 2 JSON.stringify(obj) === '{}' 复制代码
78. JSBridge是什么?
JSBridge
是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置、摄像头)。
实际上,JSBridge 就像其名称中的Bridge的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是构建 Native 和非 Native 间消息通信的通道,而且这个通信的通道是双向的。
- 双向通信的通道: JS 向 Native 发送消息: 调用相关功能、通知 Native 当前 JS 的相关状态等。
- Native 向 JS 发送消息: 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。
79. [] == ![] 结果是什么?
结果为 true
在==
(双等于) 中,左右两边都需要转换为数字然后进行比较。
[]
转换为数字为0
。![]
首先是转换为布尔值,由于[]
作为一个引用类型转换为布尔值为true
, 因此![]
为false
,进而再转换成数字,变为0
。0 == 0
, 结果为true
80. Object.is 和 === 有什么区别?
Object.is()
在 ===
(严格等于)的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。
81. instanceof 能否判断基本数据类型?
能,但是需要自定义instanceof
行为,例如:
class PrimitiveString { static [Symbol.hasInstance](x) { return typeof x === 'string' } } console.log('CoderBin' instanceof PrimitiveString) // true 复制代码
这里将原有的instanceof方法重定义,换成了typeof,因此能够判断基本数据类型。
82. typeof 能否正确判断类型?
- 对于原始类型来说,除了判断
null
类型结果是'object'
,其他的可以调用typeof
方法显示正确的类型。 - 但对于引用数据类型,除了函数之外,都会显示
'object'
。 - 因此采用
typeof
判断对象数据类型是不合适的,采用instanceof
会更好,instanceof
的原理是基于原型链的查询,只要处于原型链中,判断永远为true
83. 什么是BigInt?
BigInt
是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对 大整数 执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
84. 0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成 0.30000000000000004。
86. 说说JavaScript中常见的几种内存泄漏的情况
- 以外的全局变量
function foo(arg) { bar = "this is a hidden global variable"; } 复制代码
没有使用声明关键字的变量会被当成全局变量
- 另一种意外的全局变量可能由
this
创建:
function foo() { this.variable = "potential accidental global"; } // foo 调用自己,this 指向了全局对象(window) foo(); 复制代码
上述使用严格模式,可以避免意外的全局变量
- 定时器也常会造成内存泄露
let someResource = getData() setInterval(function() { let node = document.getElementById('Node') if (node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource) } }, 1000) 复制代码
如果id
为Node的元素从DOM
中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource
的引用,定时器外面的someResource
也不会被释放
包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放
function bindEvent() { var obj = document.createElement('XXX') var unused = function() { console.log(obj, '闭包内引用obj obj不会被释放') } obj = null // 解决方法 } 复制代码
- 没有清理对
DOM
元素的引用同样造成内存泄露
const refA = document.getElementById('refA') document.body.removeChild(refA) // dom删除了 console.log(refA, 'refA') // 但是还存在引用能console出整个div 没有被回收 refA = null console.log(refA, 'refA') // 解除引用 复制代码
包括使用事件监听addEventListener
监听的时候,在不监听的情况下使用removeEventListener
取消对事件监听
更多题库 地址:前端面试题库
87. 说说你对BOM的理解,以及常见的BOM对象有哪些?
BOM
(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率
常见的BOM对象有:
window
是BOM的核心对象,它表示浏览器的一个实例。在浏览器中,window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。因此所有在全局作用域中声明的变量、函数都会变成window
对象的属性和方法location
对象用于获取或设置窗体的 URL,并且可以用于解析 URL。navigator
对象主要用来获取浏览器的属性,区分浏览器类型。screen
对象保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度history
对象主要用来操作浏览器URL
的历史记录,可以通过参数向前,向后,或者向指定URL
跳转
88. 正则表达式是什么,有哪些应用场景?
正则表达式是一种用来匹配字符串的强有力的方法
它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的
在 JavaScript
中,正则表达式也是对象,构建正则表达式有两种方式:
- 字面量创建,其由包含在斜杠之间的模式组成
const re = /\d+/g; 复制代码
- 调用
RegExp
对象的构造函数
const re = new RegExp("\\d+","g"); const rul = "\\d+" const re1 = new RegExp(rul,"g"); 复制代码
使用构建函数创建,第一个参数可以是一个变量,遇到特殊字符需要使用\
进行转义
使用场景: 验证手机号码,邮箱,用户名等等需要一定规则的字符就可以使用正则表达式去校验
89. new操作符是什么,具体干了什么?
在JavaScript
中,new
操作符用于创建一个给定构造函数的实例对象
new
关键字主要做了以下的工作:
- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
简单实现:
// 实现new操作符 function mynew(Func, ...args) { // 1.创建一个新对象 const obj = {} // 2.新对象原型指向构造函数原型对象 obj.__proto__ = Func.prototype // 3.将构建函数的this指向新对象 let result = Func.apply(obj, args) // 4.根据返回值判断 return result instanceof Object ? result : obj } 复制代码
测试
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.say = function () { console.log(this.name) } let p = mynew(Person, "CoderBin", 18) console.log(p) // Person {name: "CoderBin", age: 18} p.say() // CoderBin 复制代码
90. 谈谈你对this对象的理解
函数的 this
关键字在 JavaScript
中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)
this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象
更多题库 地址:前端面试题库
91. 什么是作用域链?
首先先来了解什么是作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性 我们一般将作用域分成:
- 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
- 函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
- 块级作用域:ES6引入了
let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
什么是作用域链:当在Javascript
中使用一个变量的时候,首先Javascript
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
94. 说说对Websocket的了解
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
优点:说到优点,这里的对比参照物是HTTP协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。
- 支持双向通信,实时性更强。
- 更好的二进制支持。
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
- 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
95. forEach有中断效果吗?如何中断forEach循环?
在forEach
中用return
不会返回,函数会继续执行。 中断方法
- 使用
try
监视代码块,在需要中断的地方抛出异常。 - 官方推荐方法(替换方法):用
every
和some
替代forEach
函数。
every
在碰到return false
的时候,终止循环。some
在碰到return true
的时候,终止循环。
96. call、bind、apply三者的区别,如何实现?
call
、apply
、bind
作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this
指向
call
方法的第一个参数是this
的指向,后面传入的是一个参数列表。改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次bind
方法和call很相似,第一参数是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数apply
接受两个参数,第一个参数是this
的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
三者区别总结:
- 三者都可以改变函数的
this
对象指向 - 三者第一个参数都是
this
要指向的对象,如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
97. JavaScript中如何实现继承?
JavaScript实现继承有六种方法:原型链继承、盗用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承
98. JavaScript中的原型,原型链分别是什么?
原型: JavaScript` 常被描述为一种基于原型的语言——每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非实例对象本身
原型链: 原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
99. 如何判断页面是通过PC端还是移动端访问?
- 使用
navigator.userAgent
,只要里面包含mobi
、android
、iphone
等关键字,就可以认定是移动设备。这种方法的优点是简单方便,缺点是不可靠,因为用户可以修改这个字符串,让手机浏览器伪装成桌面浏览器。 - 使用
window.screen.width
,如果屏幕宽度小于500像素,就认为是手机。这个方法的缺点在于,如果手机横屏使用,就识别不了。 - 使用
window.orientation
,侦测屏幕方向,手机屏幕可以随时改变方向(横屏或竖屏),桌面设备做不到。window.orientation
属性用于获取屏幕的当前方向,只有移动设备才有这个属性,桌面设备会返回undefined
。(注意:iPhone 的 Safari 浏览器不支持该属性。) - 使用
ontouchstart
事件,手机浏览器的 DOM 元素可以通过ontouchstart
属性,为touch
事件指定监听函数。桌面设备没有这个属性。 - 使用第三方的工具包,推荐
react-device-detect
,它支持多种粒度的设备侦测。
import {isMobile} from 'react-device-detect'; if (isMobile) { // 当前设备是移动设备 } 复制代码
100. 如何让Promise.all在抛出异常后依然有效?
- 在
promise.all
队列中,使用map每一个过滤每一个promise任务,其中任意一个报错后,return一个返回值,确保promise能正常执行走到.then
中。
const p1 = new Promise((resolve, reject) => { resolve('p1'); }); const p2 = new Promise((resolve, reject) => { resolve('p2'); }); const p3 = new Promise((resolve, reject) => { reject('p3'); }); Promise.all([p1, p2, p3].map(p => p.catch(e => `出错后返回的值:${e}` ))) .then(values => { console.log(values); }).catch(err => { console.log(err); }) 复制代码
- 使用
Promise.allSettled
替代Promise.all()
。
Promise.allSettled()
方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。
101. Promist.catch后面的.then还会执行吗?
答案:.then会继续执行
虽然Promise是开发过程中使用非常频繁的一个技术点,但是它的一些细节可能很多人都没有去关注过。我们都知道.then
, .catch
, .finally
都可以链式调用,其本质上是因为返回了一个新的Promise实例。
catch的语法形式如下:
p.catch(onRejected); 复制代码
.catch
只会处理rejected
的情况,并且也会返回一个新的Promise
实例。
.catch(onRejected)
与then(undefined, onRejected)
在表现上是一致的。
事实上,catch(onRejected)从内部调用了then(undefined, onRejected)。
- 如果
.catch(onRejected)
的onRejected
回调中返回了一个状态为rejected
的Promise
实例,那么.catch
返回的Promise
实例的状态也将变成rejected
。 - 如果
.catch(onRejected)
的onRejected
回调中抛出了异常,那么.catch
返回的Promise
实例的状态也将变成rejected
。 - 其他情况下,
.catch
返回的Promise
实例的状态将是fulfilled
。
102. es5中的类和es6中的class有什么区别?
在es5中主要是通过构造函数方式和原型方式来定义一个类,在es6中我们可以通过class来定义类。它们的区别有:
- es6的class类必须new调用,不能直接执行。
- class类不存在变量提升
- class类无法遍历它实例原型链上的属性和方法
- es6为new命令引入了一个
new.target
属性,它会返回new命令作用于的那个构造函数。如果不是通过new调用或Reflect.construct()
调用的,new.target
会返回undefined - class类有static静态方法
更多题库 地址:前端面试题库
103. 简单说说前端路由?
概念:前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。
使用场景:在单页面应用,大部分页面结构不变,只改变部分内容的使用
优缺点:
- 优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户
- 缺点:单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置
实现方式:前端路由一共有两种实现方式,一种是通过 hash
的方式,一种是通过使用 pushState
的方式。
104. 什么是点击穿透,如何解决?
在发生触摸动作约300ms之后,移动端会模拟产生click动作,它底下的具有点击特性的元素也会被触发,这种现象称为点击穿透。
常见场景
- 情景一:遮罩层点击穿透问题,点击遮罩层(mask)上的关闭按钮,遮罩层消失后发现触发了按钮下面元素的click事件。
- 情景二:跨页面点击穿透问题:如果按钮下面恰好是一个有href属性的a标签,那么页面就会发生跳转。
- 情景三:另一种跨页面点击穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了。
- 情景四:新页面中对应位置元素恰好是a标签,然后就发生连续跳转了。这种情况概率很低
发生的条件
- 上层元素监听了触摸事件,触摸之后该层元素消失
- 下层元素具有点击特性(监听了click事件或默认的特性(a标签、input、button标签))
解决点击穿透的方法
- 方法一:书写规范问题,不要混用touch和click。既然touch之后300ms会触发click,只用touch或者只用click就自然不会存在问题了。
- 方法二:吃掉(或者说是消费掉)touch之后的click,依旧用tap,只是在可能发生点击穿透的情形做额外的处理,拿个东西来挡住、或者tap后延迟350毫秒再隐藏mask、pointer-events、在下面元素的事件处理器里做检测(配合全局flag)等。
105. 简单说说setTimeout的运行机制
setTimeout
和 setInterval
的运行机制,其实就是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。
这意味着,setTimeout指定的代码,必须等到本次执行的所有同步代码都执行完,才会执行。
106. Promise.all和Promise.allSettled有什么区别?
最大的区别:Promise.allSettled
永远不会被reject。
使用Promise.all
时,一旦有一个promise出现了异常,被reject了,尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了。
而Promise.allSettled
不会有这种问题,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。
107. 简单说说浏览器的垃圾回收机制有哪些?
JS会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 "垃圾回收"。
一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏"。
由于是自动回收,所以就存在一个 "内存是否需要被回收的" 的问题,但是这个问题的判定在程序中意味着无法通过某个算法去准确完整的解决,后面探讨的回收机制只能有限的去解决一般的问题。
回收算法:垃圾回收对是否需要回收的问题主要依赖于对变量的判定是否可访问,由此衍生出两种主要的回收算法:
- 标记清理
- 引用计数
标记清理:标记清理是js最常用的回收策略,2012年后所有浏览器都使用了这种策略,此后的对回收策略的改进也是基于这个策略的改进。其策略是:
- 变量进入上下文,也可理解为作用域,会加上标记,证明其存在于该上下文;
- 将所有在上下文中的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃有用;
- 在此之后再被加上标记的变量标记为准备删除的变量,因为上下文中的变量已经无法访问它们;
- 执行内存清理,销毁带标记的所有非活跃值并回收之前被占用的内存;
局限:
- 由于是从根对象(全局对象)开始查找,对于那些无法从根对象查询到的对象都将被清除
- 回收后会形成内存碎片,影响后面申请大的连续内存空间
引用计数:引用计数策略相对而言不常用,因为弊端较多。其思路是对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体的规则有:
- 声明一个变量,赋予它一个引用值时,计数+1;
- 同一个值被赋予另外一个变量时,引用+1;
- 保存对该值引用的变量被其他值覆盖,引用-1;
- 引用为0,回收内存;
局限:
最重要的问题就是,循环引用的问题
function foo() { let a = new Object() let b = new Object() a.c = b b.c = a //互相引用 } 复制代码
根据之前提到的规则,两个都互相引用了,引用计数不为0,所以两个变量都无法回收。如果频繁的调用改函数,则会造成很严重的内存泄漏。
更多题库 地址:前端面试题库
108. 箭头函数中的this指向哪里?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
可以⽤Babel理解⼀下箭头函数:
// ES6 const obj = { getArrow() { return () => { console.log(this === obj) } } } 复制代码
转换后:
// ES5,由 Babel 转译 var obj = { getArrow: function getArrow() { var _this = this return function() { console.log(_this === obj) } } } 复制代码
109. Object.assign和扩展运算符是深拷贝还是浅拷贝,两者区别是什么?
拓展运算符
let obj = { inObj: { a: 1, b: 2 } } let newObj = { ...obj } newObj.inObj.a = 2 console.log(obj) // {inObj: {a: 2, b: 2}} 复制代码
使用扩展运算符创建出新的对象,但是执行newObj.inObj.a = 2
后,原对象的里面的值也被改变了
Object.assign()
let obj = { inObj: {a: 1, b: 2} } let newObj = Object.assign({}, obj) newObj.inObj.a = 2 console.log(obj) // {inObj: {a: 2, b: 2}} 复制代码
情况和扩展运算符一样,原对象里面的值也被改变了。
所以,两者都是浅拷贝。
扩展操作符(…
)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。
Object.assign()
方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
110. 浏览器一帧都会干些什么?
我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:
- 接受输入事件
- 执行事件回调
- 开始一帧
- 执行 RAF (RequestAnimationFrame)
- 页面布局,样式计算
- 绘制渲染
- 执行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事而且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。
111. 说说Object.defineProperty与Proxy的区别?
在 Vue2.x 的版本中,双向绑定是基于 Object.defineProperty
方式实现的。而 Vue3.x 版本中,使用了 ES6 中的 Proxy
代理的方式实现。
- 使用
Object.defineProperty
会产生三个主要的问题:
- 不能监听数组的变化
- 必须遍历对象的每个属性。可以通过
Object.keys()
来实现 - 必须深层遍历嵌套的对象。通过递归深层遍历嵌套对象,然后通过
Object.keys()
来实现对每个属性的劫持
- 关于Proxy
- Proxy 针对的整个对象,
Object.defineProperty
针对单个属性,这就解决了需要对对象进行深度递归(支持嵌套的复杂对象劫持)实现对每个属性劫持的问题 - Proxy 解决了
Object.defineProperty
无法劫持数组的问题 - 比
Object.defineProperty
有更多的拦截方法,对比一些新的浏览器,可能会对 Proxy 针正对性的优化,有助于性能提升
112. base64编码图片,为什么会让数据量变大?
Base64编码的思想是是采用64个基本的ASCII码字符对数据进行重新编码。它将需要编码的数据拆分成字节数组。以3个字节为一组。按顺序排列24位数据,再把这24位数据分成4组,即每组6位。再在每组的的最高位前补两个0凑足一个字节。这样就把一个3字节为一组的数据重新编码成了4个字节。当所要编码的数据的字节数不是3的整倍数,也就是说在分组时最后一组不够3个字节。这时在最后一组填充1到2个0字节。并在最后编码完成后在结尾添加1到2个"="。
( 注BASE64字符表:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/)
从以上编码规则可以得知,通过Base64编码,原来的3个字节编码后将成为4个字节,即字节增加了33.3%,数据量相应变大。所以20M的数据通过Base64编码后大小大概为20M*133.3%=26.67M。
113. 虚拟DOM一定更快吗?
虚拟DOM/domDiff
我们常说的虚拟DOM是通过JS对象模拟出来的DOM节点,domDiff是通过特定算法计算出来一次操作所带来的DOM变化。react和vue中都使用了虚拟DOM,我们借着react聊聊虚拟DOM。
react中涉及到虚拟DOM的代码主要分为以下三部分,其中核心是第二步的domDiff算法:
- 把render中的JSX(或者createElement这个API)转化成虚拟DOM
- 状态或属性改变后重新计算虚拟DOM并生成一个补丁对象(domDiff)
- 通过这个补丁对象更新视图中的DOM节点
虚拟DOM不一定更快
干前端的都知道DOM操作是性能杀手,因为操作DOM会引起页面的回流或者重绘。相比起来,通过多一些预先计算来减少DOM的操作要划算的多。
但是,“使用虚拟DOM会更快”这句话并不一定适用于所有场景。例如:一个页面就有一个按钮,点击一下,数字加一,那肯定是直接操作DOM更快。使用虚拟DOM无非白白增加了计算量和代码量。即使是复杂情况,浏览器也会对我们的DOM操作进行优化,大部分浏览器会根据我们操作的时间和次数进行批量处理,所以直接操作DOM也未必很慢。
那么为什么现在的框架都使用虚拟DOM呢?因为使用虚拟DOM可以提高代码的性能下限,并极大的优化大量操作DOM时产生的性能损耗。 同时这些框架也保证了,即使在少数虚拟DOM不太给力的场景下,性能也在我们接受的范围内。
而且,我们之所以喜欢react、vue等使用了虚拟DOM框架,不光是因为他们快,还有很多其他更重要的原因。例如react对函数式编程的友好,vue优秀的开发体验等,目前社区也有好多比较这两个框架并打口水战的,我觉着还是在两个都懂的情况下多探究一下原理更有意义一些。
114. html文档渲染过程,css文件和js文件的下载,是否会阻塞渲染?
CSS阻塞
- css 文件的下载和解析不会影响 DOM 的解析,但是会阻塞 DOM 的渲染。因为 CSSOM Tree 要和 DOM Tree 合成 Render Tree 才能绘制页面。
- css 文件没下载并解析完成之前,后续的 js 脚本不能执行。
- css 文件的下载不会阻塞前面的 js 脚本执行。(所以在需要提前执行不操作 dom 元素的 js 时,不妨把 js 放到 css 文件之前。)
js阻塞
js 文件的下载和解析会阻塞 GUI 渲染进程,也就是会阻塞 DOM 和 CSS 的解析和渲染。
- js 文件没下载并解析完成之前,后续的 HTML 和 CSS 无法解析
- js 文件的下载不会阻塞前面 HTML 和 CSS 的解析(当js放在body底部时)
115. JavaScript对象中,可枚举性(enumerable)是什么?
可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for...in
循环之中(除非属性名是一个Symbol)。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性。
for..in
循环Object.keys
方法JSON.stringify
方法
var o = { a: 1, b: 2 } o.c = 3 // 添加 d 属性,值为 4,将 enumerable 为设为 false Object.defineProperty(o, 'd', { value: 4, enumerable: false }) console.log(o.d); // 下面的方法取不到 o.d 的值 for (var key in o) console.log(o[key]) // 1 // 2 // 3 console.log(Object.keys(o)) // ["a", "b", "c"] console.log(JSON.stringify(o)); // => "{a:1,b:2,c:3}" 复制代码
上面代码中,d 属性的enumerable
为false
,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但还是可以直接获取它的值。
至于for...in
循环和Object.keys
方法的区别,在于前者包括对象继承自原型对象的属性,而后者只包括对象本身的属性。如果需要获取对象自身的所有属性,不管enumerable
的值,可以使用Object.getOwnPropertyNames
方法。
可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性。对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty
等定义的属性,该标识值默认为 false。
116. Object.create和new有什么区别?
js中创建对象的方式一般有两种 Object.create
和 new
const Base = function() {} const o1 = Object.create(Base) const o2 = new Base() 复制代码
在讲述两者区别之前,我们需要知道:
- 构造函数Foo的原型属性
Foo.prototype
指向了原型对象。 - 原型对象保存着实例共享的方法,有一个指针
constructor
指回构造函数。 - js中只有函数有
prototype
属性,所有的对象只有 proto 隐式属性。
那这样到底有什么不一样呢?
- 先来看看
Object.create
的实现方式
Object.create = function(o) { var F = function() {} F.prototype = o return new F() } 复制代码
可以看出来。Object.create
是内部定义一个对象,并且让F.prototype
对象 赋值为引进的对象/函数 o,并return
出一个新的对象。
- 再看看
const o2 = new Base()
的时候,new做了什么。
var o1 = new Object(); o1.[[Prototype]] = Base.prototype; Base.call(o1); 复制代码
new做法是新建一个obj对象o1,并且让o1的__proto__
指向了Base.prototype
对象。并且使用 call 进行强转作用环境。从而实现了实例的创建。
区别:看似是一样的。我们对原来的代码进行改进一下。
var Base = function () { this.a = 2 } var o1 = new Base(); var o2 = Object.create(Base); console.log(o1.a); // 2 console.log(o2.a); // undefined 复制代码
可以看到Object.create 失去了原来对象的属性的访问。再进行下改造:
var Base = function() { this.a = 2 } Base.prototype.a = 3 var o1 = new Base() var o2 = Object.create(Base) console.log(o1.a) // 2 console.log(o2.a) // undefined 复制代码
总结
比较 | new | Object.create |
构造函数 | 保留原构造函数属性 | 丢失原构造函数属性 |
原型链 | 原构造函数prototype属性 | 原构造函数/(对象)本身 |
作用对象 | function | function和object |
更多题库 地址:前端面试题库
117. 为什么部分请求中,参数需要使用encodeURIComponent进行转码?
一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。
这是因为网络标准RFC 1738做了硬性规定:
"...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."
这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致"URL编码"成为了一个混乱的领域。有没有办法,能够保证客户端只用一种编码方法向服务器发出请求?
就是使用Javascript先对URL编码,然后再向服务器提交,不要给浏览器插手的机会。因为Javascript的输出总是一致的,所以就保证了服务器得到的数据是格式统一的。
118. 箭头函数和普通函数有什么区别?
- 语法更加简洁、清晰
- 箭头函数不会创建自己的this(重点!)
箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
- 箭头函数继承而来的this指向永远不变(重点!)
.call()/.apply()/.bind()
无法改变箭头函数中this的指向- 箭头函数不能作为构造函数使用
- 箭头函数没有自己的
arguments
。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。 - 箭头函数没有原型
prototype
- 箭头函数不能用作
Generator
函数,不能使用yeild
关键字
119. WebSocket中的心跳机制是为了解决什么问题?
- 为了定时发送消息,使连接不超时自动断线,避免后端设了超时时间自动断线。所以需要定时发送消息给后端,让后端服务器知道连接还在通消息不能断。
- 为了检测在正常连接的状态下,后端是否正常。如果我们发了一个定时检测给后端,后端按照约定要下发一个检测消息给前端,这样才是正常的。如果后端没有正常下发,就要根据设定的超时进行重连。
120. async/await 和 Promise 有什么关系?
Promise
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
async/await
es2017的新语法,async/await
就是generator + promise的语法糖
async/await
和 Promise 的关系非常的巧妙,await必须在async内使用,并装饰一个Promise对象,async返回的也是一个Promise对象。
async/await
中的return/throw
会代理自己返回的Promise的resolve/reject
,而一个Promise的resolve/reject
会使得await得到返回值或抛出异常。
- 如果方法内无await节点
- return 一个字面量则会得到一个
{PromiseStatus: resolved}
的Promise。 - throw 一个Error则会得到一个
{PromiseStatus: rejected}
的Promise。
- 如果方法内有await节点
async
会返回一个{PromiseStatus: pending}
的Promise(发生切换,异步等待Promise的执行结果)。Promise
的resolve
会使得await
的代码节点获得相应的返回结果,并继续向下执行。Promise
的reject
会使得await
的代码节点自动抛出相应的异常,终止向下继续执行。
121. Promise中,resolve后面的语句是否还会执行?
会被执行。如果不需要执行,需要在 resolve 语句前加上 return。
122. CSR和SSR分别是什么?
CSR: 对于html的加载,以React为例,我们习惯的做法是加载js文件中的React代码,去生成页面渲染,同时,js也完成页面交互事件的绑定,这样的一个过程就是CSR(客户端渲染)。
SSR: 但如果这个js文件比较大的话,加载起来就会比较慢,到达页面渲染的时间就会比较长,导致首屏白屏。这时候,SSR(服务端渲染)就出来了:由服务端直接生成html内容返回给浏览器渲染首屏内容。
但是服务端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入js文件来辅助实现,我们把页面的展示内容和交互写在一起,让代码执行两次,这种方式就叫同构。
CSR和SSR的区别在于,最终的html代码是从客户端添加的还是从服务端。
123. 什么是内存泄漏,什么原因导致的?
内存泄露的解释:程序中己动态分配的堆内存由于某种原因未释放或无法释放。
- 根据JS的垃圾回收机制,当内存中引用的次数为0的时候内存才会被回收
- 全局执行上下文中的对象被标记为不再使用才会被释放
内存泄漏的几种场景:
- 全局变量过多。通常是变量未被定义或者胡乱引用了全局变量
- 闭包。 未手动解决必包遗留的内存引用。
- 事件监听未被移除
- 缓存。建议所有缓存都设置好过期时间。
124. web常见的攻击方式有哪些?以及如何进行防御?
常见的攻击方式有:XSS、CSRF、SQL注入
125. 什么是微前端?
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
各个前端应用还可以独立运行、独立开发、独立部署。
微前端不是单纯的前端框架或者工具,而是一套架构体系,
由于篇幅原因,下半部分内容将
「2022」JavaScript最新高频面试题指南(下)
发布。持续更新中...
每文一句:星星使天空绚烂夺目;知识使人增长才干。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
最后给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库