前言
面试其实也是工作经验的总结,在工作之余,将工作中的问题记录总结,也是为下一份工作面试打好基础。
常规面试问题
1、项目中的跨域如何处理的。
跨域是一个比较大的话题,问题本身在开发过程中比较常见的问题,
对跨域的理解和解决方法可以点击这里
2、防抖与节流,什么场景用。
防抖:防止抖动、阻止用户行为多次触发请求。即是高频触发的事件,一定时间内、只有最后一次被触发生效。
使用场景:
- 监听页面数据变化重置页面布局,如:调整浏览器大小。
- 登录、发送短信、支付等避免用户点击太快导致多次触发请求。
- 表单、文档实现自动保存,监听在没任何操作后多长时间进行自动保存。
代码实现封装防抖:
function dobunce (fn, waitTime){
let timer = null;
return (...arg) => {
if(timer) clearTimeout(timer)
timer = setTimerout(()=>{
fn(...arg)
},waitTime)
}
}
节流:节约流量、控制事件的发生频率,即是高频触发的事件、一定时间内只触发一次。
使用场景:
- 输入框实时输入搜索展示下拉框。
- 监听滚动条到指定位置触发加载更多。
- 每隔多长时间记录鼠标位置,
代码实现:
function throttle(fn,waitTime){
let timer = null;
return (...args)=>{
if(timer) return
timer = setTimeout(()=>{
fn(...args)
timer=null
}, waitTime)
}
}
3、HTTP协议 的理解。
4、懒加载如何判断元素出现在视口内?
判断元素是否在视口内:需要了解浏览器判断一个元素位置的方法。首先我们需要了解怎么判断浏览器的宽、高。
//兼容所有浏览,获取浏览器的宽高的方法;
const w=window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const h=window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
//window 常用的方法
window.close() //关闭浏览器窗口
window.open() // 开启浏览器窗口
window.resizeTo() // 调整浏览器窗口
window.moveTo() //移动浏览器
同时我们还需要了解元素属性;
getBoundingClientRect();
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。拥有left, top, right, bottom, x, y, width, height属性。
使用 getBoundingClientRect 方法 判断元素是否在视口内部、公式为:
el.getBoundingClientRect().top < viewHight(视口高度)
代码实现:
const elIsInViewPort = (el) => {
const viewHight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
const elToViewHeight = el.getBoundingClientRect()?.top;
return elToViewHeight <= viewHight + 100 //+100 目的是为了提前加载数据
}
IntersectionObserver 接口;
IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素
顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)
了解更多 IntersectionObserver API 移步到这里
使用 IntersectionObserver 判断元素是否在视口内 代码实现方法
//
const intersectionObserver = new IntersectionObserver((entries)=>{
// 如果 intersectionRatio 为 0,则目标在视野外,
if(entries.length < 1) return
entries.forEatch((target)=>{
if(target.isIntersecting){
//停止对元素的观察
intersectionObserver.unobserve(target)
}
})
})
const elArr = [...document.querySelectorArr('.selectEle')]
elArr.forEatch((item)=>{
intersectionObserver.observe(item) //对元素进行监听;
})
5、ES6中let、const与ES5 var的区别。
let ,const, var 都是js 中使用来申明变量的方法。
var 声明的变量 为全局变量,及是 window 下的变量,存在变量提升问题。
let、 const 为es6 新提出的申明变量的方法,申明的变量为块级作用域、
const 声明 的变量为常量、必须初始化值、不能修改变量值、否则会报错。
let 申明的块级作用域变量,可以修改变量值。
let const 声明的变量都不能重复声明,重复声明会报错。var 声明重复变量不报错会覆盖前面变量的值。
6、Promise的理解。
promise js 使用来异步执行代码的方法,由于js 执行代码为同步的,一次只执行一次任务,阻塞其他任务。
7、JavaScript有哪些数据类型?如何判断这些类型。
js中数据类型:
类型 | typeOf 返回值 |
---|---|
Null | “Object” |
Undefined | “Object” |
Boolean | “boolean” |
Number | “number” |
BigInt | “bigint” |
String | “string” |
Symbol | “symbol” |
判断数据类型方法
方法一:typeOf
能够精确的返回数据的基本数据类型,但是不能明确判断出, Null、 Underfined 对象值,
方法二: instanceOf
可以拿根据 instanceOf 判断对象是继承哪一个原始类型,判断方法为根据js原型链特性,逐级向上找到根节点
代码实现instanceOf
const myInstanceOf = (obj,type) =>{
if(typeOf obj !== 'object' || obj == null) return false
let proto = Object.getProtoTypeOf(obj)
while(true){
if(proto === null) return false
else if(proto === type.protoType){
return true
}else {
proto = Object.getProtoTypeOf(proto)
}
}
}
//使用方法
const s = 'abc'
myInstanceOf(s, 'string') // true
方法三: Object.protoType.toString.call(obj)
toString 方法为对象原型上的方法,直接调用toString 方法 返回 “[object: ‘Xxxxx’]” , ‘Xxxxx’ 就为对象的类型,object 调用toString 方法 直接返回 “[object: ‘object’]” 字符串, 其他类型的值调用需要使用call() 调用才能返回正确的值。
Object.protoType.toString({}) // "[object Object]"
Object.protoType.toString.call('str') //"[object String]"
Object.protoType.toString.call(124) // "[object Number]"
其他也可以使用 数据类型的转换
如: Boolean() 类型转换 Number() 类型转换
8、Axios与Ajax的区别。
Axios和Ajax是两种不同的技术,用于实现前后端数据交互。
-
Axios是一个基于Promise的HTTP客户端,可以用于浏览器和Node.js环境。它支持跨域请求、拦截请求和响应、取消请求等功能。Axios可以通过简单的API发送HTTP请求,并且可以处理响应结果。它是一个相对较新的技术,逐渐取代了旧的Ajax技术。
-
Ajax(Asynchronous JavaScript and XML)是一种用于创建异步请求的技术。它可以使网页在不重新加载的情况下更新部分内容,提升用户体验。Ajax可以使用XMLHttpRequest对象来发送HTTP请求,并通过回调函数处理响应结果。
所以,主要区别如下:
- Axios是一个基于Promise的HTTP客户端,而Ajax是一种用于创建异步请求的技术。
- Axios可以用于浏览器和Node.js环境,而Ajax只能用于浏览器环境。
- Axios支持更多的功能,如跨域请求、拦截请求和响应、取消请求等。
- 使用Axios时,可以通过简单的API发送HTTP请求,而使用Ajax需要手动创建XMLHttpRequest对象。
- Axios相对来说是较新的技术,逐渐取代了Ajax的使用。
9、HTML中的DOM操作是指使用JavaScript来操作HTML文档中的各个元素和属性。DOM(文档对象模型)提供了一种访问和操作HTML文档的标准方式。
通过DOM操作,可以实现以下功能:
-
获取元素:使用document对象的方法,如getElementById、getElementsByTagName、getElementsByClassName等来获取HTML文档中的元素。
-
修改元素的内容:使用innerHTML属性来修改元素的内容,或使用textContent属性来修改文本内容。
-
修改元素的样式:使用style属性来修改元素的样式,如颜色、背景、字体等。
-
修改元素的属性:使用setAttribute方法来修改元素的属性,如class、id、src等。
-
创建和删除元素:使用createElement方法创建新的元素,并使用appendChild或insertBefore方法将其插入到指定位置,使用removeChild方法将元素从文档中删除。
-
监听事件:使用addEventListener方法来监听元素上的事件,如点击、鼠标移动等。
-
修改表单数据:可以使用JavaScript来获取和修改表单元素的值,如input、select、textarea等。
通过以上DOM操作,可以实现动态地更新HTML文档的内容、样式和行为,使网页具有更好的交互性和用户体验。HTML中的DOM操作。
10、普通函数和自定义hook的区别。
-
使用方式:
- 普通函数可以在任何地方被调用,可以接受任意参数并返回一个值。
- 自定义hook只能在函数组件中使用,并且必须按照规定的方式调用和使用返回的值。
-
设计思路:
- 普通函数通常用于解决具体的问题,实现一些特定的功能逻辑。
- 自定义hook是一种将复用逻辑进行抽象和封装的方式,它可以将组件之间的共享逻辑提取出来,并使其更容易被理解、测试和维护。
-
命名规范:
- 普通函数没有特定的命名规范,可以根据需求起一个合适的名称。
- 自定义hook的命名必须以"use"开头,这是React的约定,以便开发者能够识别出它们是hooks。
总结起来,普通函数主要用于解决具体问题,而自定义hook主要用于复用和共享逻辑。自定义hook的设计目标是将组件之间的逻辑提取出来,使得逻辑更加清晰、可重用,并且符合React的设计理念。普通函数与自定义hook的区别。
11、SSR服务端渲染的理解。
SSR(Server-Side Rendering)是一种Web开发技术,它在服务器端生成完整的HTML内容,然后将其发送到客户端浏览器进行展示。与传统的客户端渲染(CSR)不同,客户端渲染是指在浏览器中使用JavaScript来动态生成HTML内容。
SSR的主要优势是改善了网页的加载性能和搜索引擎优化(SEO)。由于页面在服务器端生成,用户在请求页面时会直接获得完整的HTML内容,因此首次加载速度比CSR更快,且可以提供更好的搜索引擎抓取和索引。
SSR的实现需要使用一些特定的框架或库,如React的Next.js、Vue的Nuxt.js等。这些工具提供了服务端渲染的功能,可以将组件在服务器端渲染为HTML,并将其与浏览器端的JavaScript进行关联,以实现客户端的交互功能。
SSR的基本流程包括以下几个步骤:
- 服务器接收到请求,并根据请求的URL确定需要渲染的组件。
- 服务器使用组件渲染引擎(如React的服务器端渲染引擎)将组件渲染为HTML字符串。
- 服务器将生成的HTML字符串作为响应发送给客户端浏览器。
- 客户端浏览器接收到HTML响应后,会执行其中的JavaScript代码并构建页面的DOM结构。
- 客户端渲染工具会接管页面的控制,并处理用户的交互行为,如点击、输入等。
- 当用户进行交互时,客户端会通过Ajax或WebSocket等技术与服务器进行通信,获取数据并更新页面。
需要注意的是,由于SSR需要在服务器端进行组件渲染,因此对服务器的负载有一定的影响。此外,SSR也增加了开发的复杂性,因为需要同时处理服务器端和客户端的渲染逻辑。
12、浏览器如何做静态资源缓存?
浏览器可以通过以下几种方式进行静态资源缓存:
-
设置HTTP缓存头:浏览器通过设置响应头中的缓存相关字段,指示浏览器对静态资源进行缓存。常用的响应头字段有:
- Expires: 指定静态资源的过期时间,浏览器在过期时间之前不会重新请求该资源。
- Cache-Control: 控制缓存的行为,如设置为
max-age=3600
表示资源在一个小时内有效。 - Last-Modified/If-Modified-Since: 服务器返回资源的最后修改时间,浏览器在下一次请求时通过
If-Modified-Since
字段将上次的最后修改时间发送给服务器,如果资源在服务器上的最后修改时间仍然一致,服务器会返回一个304 Not Modified
的响应,表示资源未变化,浏览器可以使用缓存的版本。
-
ETag/If-None-Match: 服务器对于每个资源生成一个唯一的标识符,称为ETag,浏览器在下一次请求时通过
If-None-Match
字段将上次请求中服务器返回的ETag发送给服务器,如果这个资源的ETag仍然一致,服务器会返回一个304 Not Modified
的响应。 -
Service Worker缓存:通过Service Worker技术,浏览器可以在离线状态或网络不稳定的情况下,拦截对静态资源的请求并返回缓存的版本。
-
localStorage/IndexedDB: 将静态资源缓存到本地存储(localStorage或IndexedDB)中,下次使用时直接从本地读取。
需要注意的是,浏览器缓存是由浏览器自行处理的,开发人员只需在服务器端设置合适的响应头字段和缓存策略即可。另外,浏览器缓存只适用于静态资源,动态资源每次访问都会向服务器发起请求。
13、样式覆盖如何处理?
在前端开发中,样式覆盖是处理样式冲突的重要问题。以下是一些常见的处理样式覆盖的方法:
-
使用更具体的选择器:如果多个样式规则应用于同一个元素,可以使用更具体的选择器来指定优先级。例如,使用id选择器比class选择器具有更高的优先级。
-
使用!important:使用!important声明可以将样式规则的优先级提升到最高级别,但应谨慎使用此方法,因为它可能会导致样式无法继承和覆盖。
-
修改HTML结构:通过修改HTML结构可以改变元素的嵌套关系,从而达到覆盖样式的目的。例如,将要覆盖样式的元素移到不受其他样式影响的位置。
-
使用内联样式:通过将样式规则直接写在元素的style属性中,可以覆盖外部样式表中的样式。但这种方法比较繁琐,而且不易维护。
-
使用CSS权重:CSS权重是指样式规则的优先级,根据选择器的权重大小来决定样式的应用顺序。可以利用选择器的权重来控制样式覆盖的顺序。
-
使用CSS预处理器:使用CSS预处理器(如Sass或Less)可以更方便地管理样式覆盖问题。预处理器提供了变量、混合器、嵌套等功能,可以更灵活地控制样式的应用和覆盖。
14、H5 与手机是如何通信的?
H5与手机通信主要通过以下几种方式实现:
-
WebSocket:WebSocket是一种持续连接的通信协议,能够在浏览器和服务器之间进行双向通信。H5可以通过WebSocket与后台服务器建立连接,实现实时通信。
-
AJAX:AJAX(Asynchronous JavaScript and XML)是一种通过在后台与服务器进行少量数据交换,更新部分网页的技术。H5可以通过AJAX异步请求与后台服务器进行通信,获取数据并更新页面内容。
-
WebRTC:WebRTC(Web Real-Time Communication)是一种浏览器原生支持的实时通信技术。H5可以使用WebRTC与其他设备进行音视频通话、数据传输等。
-
推送通知:H5可以通过推送通知技术与手机进行通信。例如,使用Firebase Cloud Messaging(FCM)或苹果推送通知服务(APNs)等进行消息推送。
-
调用原生接口:H5可以通过调用手机操作系统提供的原生接口与手机进行通信。通过类似于Cordova或React Native等框架,H5可以调用手机的相机、地理位置、传感器等功能。
总的来说,H5与手机通信的方式多种多样,可以根据具体需求选择合适的通信方式。
15、如何判断是手机端还是PC端?
可以通过以下几种方式来判断是手机端还是PC端:
屏幕尺寸:手机屏幕通常较小,一般在4-7寸之间,而PC屏幕通常较大,一般在13寸及以上。可以通过浏览器的window.innerWidth和window.innerHeight属性来获取当前窗口的宽度和高度,从而判断是手机还是PC。
用户代理(User Agent)字符串:浏览器在每次请求网页时会发送一个用户代理字符串,其中包含了浏览器的信息,包括操作系统和设备类型。可以通过检查用户代理字符串中是否包含关键字如"Mobile"或"Android"等来判断是手机端还是PC端。
媒体查询:可以利用CSS3的媒体查询来对不同设备进行样式适配。通过设置不同的CSS样式表或样式规则,根据屏幕尺寸、像素密度等参数来判断是手机端还是PC端,并给予不同的样式。
触摸事件:手机设备支持触摸事件,而PC设备通常不支持。可以通过判断是否支持触摸事件来判断是手机端还是PC端。
16、Event Loop事件轮询机制。
事件循环(Event Loop)是一种在单线程环境下处理多个任务的机制。它通过不断地循环遍历任务队列,将任务分发到可执行的线程或处理器上执行,从而实现并发处理。
在前端开发中,事件循环常常用于处理用户交互、网络请求和定时器等异步任务。它的基本原理是将异步任务注册到事件队列中,然后通过事件循环机制不断地检查队列中是否有事件需要处理,如果有则执行相应的回调函数。
事件循环机制可以分为以下几个步骤:
- 将异步任务注册到事件队列中,例如用户点击事件、网络请求完成事件等。
- 主线程首先执行所有的同步任务,然后开始循环遍历事件队列。
- 如果事件队列为空,则继续等待事件的触发。
- 如果事件队列中有事件需要处理,则取出事件并执行对应的回调函数。
- 执行完回调函数后,继续循环遍历事件队列,直到队列为空或程序退出。
事件循环机制的优点是能够提高程序的响应速度和并发性能,同时避免了线程切换的开销。然而,由于事件循环是在单线程中执行的,如果某个任务执行时间过长,会阻塞后续任务的执行,导致程序出现卡顿或无响应的情况。因此,在编写异步代码时需要注意控制任务的执行时间,尽量避免长时间的阻塞操作。
17、无状态组件、有状态组件。
无状态组件是指没有内部状态(state)的组件,它只接受外部传入的props并根据传入的props渲染界面。无状态组件通常是函数式组件(Functional Component),使用函数来定义,没有生命周期方法。
有状态组件是指具有内部状态的组件,它可以存储、修改和使用内部的状态数据。有状态组件通常是类组件(Class Component),使用类来定义,可以使用生命周期方法来处理数据的获取、更新和渲染等操作。
在React中,推荐使用无状态组件来定义简单的展示型组件,而使用有状态组件来定义复杂的交互型组件。无状态组件具有较简洁的代码和更好的性能,因为它没有内部状态需要管理,而有状态组件具有更多的功能和灵活性,可以处理复杂的业务逻辑。
18、箭头函数与普通函数的区别?本质区别是什么?
箭头函数与普通函数的区别有以下几点:
-
语法简洁:箭头函数使用更简洁的语法,可以省略function关键字和return关键字。
-
this绑定:箭头函数没有自己的this值,它会捕获所在上下文的this值,因此无法使用call、apply和bind方法来改变this的指向。
-
arguments对象:箭头函数没有自己的arguments对象,它在定义时会继承所在上下文的arguments对象。
-
构造函数:箭头函数不能用作构造函数,不能通过new关键字来实例化,并且没有prototype属性。
-
返回值:箭头函数只能有一个表达式,该表达式会被隐式地返回,而普通函数可以有多个语句和多个返回值。
箭头函数的本质区别在于this的绑定机制不同。普通函数中的this值是在函数被调用时才确定的,它可以根据函数的调用方式动态地改变。而箭头函数的this值是在函数定义时就确定的,并且不会因为函数的调用方式而改变,它会捕获所在上下文的this值。这使得箭头函数在处理this的问题上更加简单和可靠。
19、Redux工作流。
Redux工作流是一种用于管理应用数据状态的工作流程。它基于以下几个核心概念:
-
Store(仓库):应用的数据状态存储在一个单一的仓库中,仓库是Redux的核心。它包含了应用的所有状态数据。
-
Action(动作):动作是触发状态变化的事件,它是一个带有
type
属性的普通对象。动作描述了应用的状态变化,比如添加、删除或更新数据等。 -
Reducer(减速器):减速器是一个纯函数,它接收一个当前状态和一个动作对象,并根据动作的类型来返回一个新的状态。它的作用是将动作应用于当前的状态,返回一个新的状态,而不是直接修改原来的状态。
-
Dispatch(派发):派发是触发动作的过程,应用中的任何一个组件都可以派发一个动作,通过调用
dispatch
方法来触发状态变化。 -
Subscribe(订阅):订阅是监听状态变化的过程,可以通过调用
store.subscribe()
方法来注册一个回调函数,当状态发生变化时,订阅的回调函数将被调用。
Redux工作流的基本思想是将应用的状态存储在一个单一的仓库中,并通过派发动作来触发状态的变化,然后通过减速器将动作应用于状态,最后通知订阅者状态的变化。这种工作流的好处是使状态变化可预测、可追踪,简化了状态管理的复杂性,并且方便与其他组件进行交互和共享状态数据。
20、 移动端适配怎么做?
移动端适配是指将网站或应用程序在不同尺寸的移动设备上展示和使用时,保持良好的用户体验和界面布局的处理方法。
以下是移动端适配的一些常见做法:
-
使用响应式布局:通过设置页面元素的百分比宽度、最大和最小宽度等属性,使页面能够根据不同设备的屏幕大小自动调整布局。
-
使用媒体查询:媒体查询是CSS3的特性,通过查询设备的屏幕宽度、高度和方向等信息,从而为不同设备应用不同的样式规则。
-
使用流式布局:流式布局是一种相对于固定宽度布局的一种布局方式,页面元素的宽度是相对于父容器的宽度进行调整的,从而适应不同设备尺寸。
-
使用视口相关的标签和属性:通过设置HTML页面中的meta标签的viewport属性,可以控制页面在移动设备上的缩放和布局行为。
-
使用CSS中的flexbox布局:flexbox是CSS3中的一种新的布局模型,通过使用弹性盒子的概念,可以方便地实现移动端的自适应布局。
-
使用移动端框架:如Bootstrap、Foundation等,这些框架已经内置了移动端适配的功能和样式,可以快速地进行移动端开发。
综上所述,移动端适配需要结合CSS布局、媒体查询、视口设置和框架等技术和方法,从而实现页面在不同设备上的良好展示和用户体验。
21、如何实现全网置灰。
要实现全网置灰,可以按照以下步骤进行操作:
-
了解目标网站:首先,你需要了解目标网站的结构和技术栈。这样可以更好地理解网站的各个部分,并确定如何实施置灰操作。
-
使用浏览器插件:一种简单的方法是使用浏览器插件来实现置灰。例如,可以使用类似于"Dark Reader"的插件,它可以在浏览器中为所有网站提供黑暗模式,并实现置灰效果。
-
使用脚本注入技术:如果你是一名开发人员,可以使用脚本注入技术来实现全网置灰。例如,你可以使用JavaScript编写一个脚本,并将其注入到浏览器中,使其在每个网页加载时自动执行。这个脚本可以通过操作DOM元素的样式来实现置灰效果。
-
使用代理服务器:另一种方法是使用代理服务器来实现全网置灰。你可以设置一个代理服务器,将所有网络请求重定向到该服务器。然后在服务器上修改响应,将网页的样式改为置灰效果,并将修改后的响应返回给客户端。
注意事项:
- 置灰可能会影响网站的可用性和用户体验,请谨慎使用,并遵守相关法律和规定。
- 在进行任何修改之前,务必获得网站所有者的许可。
- 网站的结构和技术可能会有所变化,因此在实施置灰操作时需要进行适当的调整和测试。
react 常见面试题
1、React 的类组件与函数式组件什么区别?
2、React源码读过吗?读了哪些?
3、 React 中的组件通信。
4、React Hook的副作用。
5、React 里面的优化点。
6、React Portal 的理解与使用。
7、useEffect和useLayoutEffect区别
8、react如何实现的中断可恢复更新
9、hooks和class的区别(hooks解偶)
10、diff原理、fiber原理
11、hooks如何模拟didupdate生命周期
vue 常见面试题
1、vue3.0 中Composition Api 与vue2.0 中 Composition Api 有什么不同
2、vue3.0 中 Treeshaking 特性,举例说明
3、vue3.0 中 为什么要使用proxy api 替代 definedproperty API
Typescript 面试题
基础知识方面
1、如何检测资源加载时长。
2、如何检测首屏、白屏渲染时长。
3、js加载执行:async, defer, preload, prefetch的区别?
4、浏览器从输入url到渲染的过程?
5、浏览器的进程与线程
6、错误捕获
7、webpack优化手段
8、如何监听到某个资源加载失败以及要怎么处理?
9、jsBridge的原理
10、 浏览器合成层
11、css加载会阻塞dom渲染吗?会阻塞js执行吗?
12、h5优化手段
13、强缓存、协商缓存与cdn缓存的区别
网络知识
1、http缓存
HTTP缓存是指浏览器通过保存之前请求的资源,以便在后续请求时进行重用的一种机制。使用HTTP缓存可以减少请求的数量,提高网页加载速度。
HTTP缓存分为两种类型:强缓存和协商缓存。
强缓存是通过设置HTTP响应头中的Cache-Control和Expires字段来实现的。当浏览器发起请求时,会先检查缓存是否过期,如果没有过期,则直接从缓存中获取资源,不会向服务器发送请求。
协商缓存是通过设置HTTP响应头中的ETag和Last-Modified字段来实现的。当浏览器发起请求时,会先向服务器发送一个If-None-Match或者If-Modified-Since字段,服务器会根据这些字段的值来判断资源是否有更新,如果没有更新,则返回一个304 Not Modified的响应,浏览器从缓存中获取资源。
使用HTTP缓存可以有效减轻服务器的负载,提高网站的性能和用户的体验。但是需要注意的是,缓存策略要根据具体的需求来定制,不同类型的资源可能需要不同的缓存时间和策略。
2、https原理
HTTPS(Hyper Text Transfer Protocol Secure)是一种用于保护网络通信的安全协议,它基于HTTP协议,并通过SSL或TLS协议进行加密和认证。
HTTPS的工作原理如下:
-
客户端发出HTTPS请求:当客户端要访问一个HTTPS网站时,它会向服务器发送一个HTTPS请求,这个请求与HTTP请求基本相同,只是URL的开头是以https://开头。
-
服务器发送数字证书:服务器收到客户端的HTTPS请求后,会将自己的数字证书发送给客户端。数字证书是由可信的证书机构(CA)颁发的,用于证明服务器的身份和公钥的有效性。
-
客户端验证数字证书:客户端收到服务器发送的数字证书后,会验证证书的有效性。它会检查证书的签名是否正确、证书是否过期以及证书中的域名是否与访问的域名一致等。如果验证通过,客户端会继续进行后续操作。
-
客户端生成密钥:客户端生成一个用于加密通信的对称密钥,并使用服务器的公钥进行加密。这个对称密钥在后续的通信中用于对数据进行加密和解密。
-
服务器解密密钥:服务器收到客户端发送的加密的对称密钥后,使用自己的私钥进行解密,得到对称密钥。
-
客户端和服务器进行加密通信:客户端和服务器双方都拥有了相同的对称密钥,它们使用这个对称密钥进行加密和解密通信的数据。
通过以上步骤,HTTPS实现了客户端和服务器之间的安全通信。由于通信数据是经过加密的,在传输过程中被截获的数据也无法被解密,从而确保了通信的机密性。同时,数字证书的验证也确保了通信的可信性,防止了中间人攻击。
3、解决跨域请求头都需要设置什么
4、cookie、session、localStorage分别是什么?有什么作用?
5、什么情况下会发送预检请求?如何优化?
手写实现
1、实现flat
使用栈的思路实现 flat
const flat = ( arr ) => {
const result = [];
const tempArr = [].concat(arr);
while tempArr.length>0:
const itemArr = tempArr.pop()
if(Array.isArray(itemArr)){
tempArr.push(...itemArr)
}else{
result.unshift(itemArr)
}
return result
}
使用reduce 实现 flat
const flat = (arr) => {
return arr.reduce((pre,next)=>{
return pre.concat(Array.isArray(next) ? flat(next) : next)
}, [])
}
2、 实现promise.all
Promise.all是一个静态方法,用于将多个Promise对象包装成一个新的Promise对象。这个新的Promise对象在所有的输入Promise对象都执行完成后才会被resolve,如果其中一个Promise对象被reject,则新的Promise对象会立即被reject,并返回被reject的Promise对象的结果。
以下是一个实现Promise.all的示例代码:
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('promises must be an array'));
}
let count = promises.length;
const results = [];
if (count === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then((result) => {
results[index] = result;
count--;
if (count === 0) {
resolve(results);
}
}).catch(reject);
});
});
}
使用示例:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
promiseAll([promise1, promise2, promise3]).then((results) => {
console.log(results); // [1, 2, 3]
}).catch((error) => {
console.error(error);
});
注意:以上实现中,如果传入的promises数组中存在非Promise对象,会先通过Promise.resolve方法将其转为Promise对象再进行处理。
3、 实现new
实现"new"操作符,可以从给定的构造函数创建一个新对象。在JavaScript中,我们可以使用"new"操作符来实例化对象,它的语法是new ConstructorName()
,其中ConstructorName是一个构造函数。
以下是一个实现"new"操作符的简单示例:
function myNew(constructor, ...args) {
// 创建一个空对象,它继承自构造函数的原型
let newObj = Object.create(constructor.prototype);
// 将构造函数的this绑定到新对象上,以便构造函数中的this指向新对象
let result = constructor.apply(newObj, args);
// 如果构造函数返回了一个对象,则返回该对象,否则返回新对象
if (typeof result === 'object' && result !== null) {
return result;
}
return newObj;
}
使用示例:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
}
let person = myNew(Person, 'Alice', 25);
person.sayHello(); // 输出: Hello, my name is Alice
console.log(person.age); // 输出: 25
这个示例中,我们使用myNew函数来创建一个Person对象,它接受构造函数和构造函数的参数作为参数。myNew函数首先创建一个空对象,该对象继承自构造函数的原型。然后,它使用apply方法将构造函数的this绑定到新对象上,并将构造函数的参数传递给它。最后,如果构造函数返回了一个对象,myNew函数将返回该对象,否则返回新创建的对象。
4、订阅发布
实现订阅发布可以使用观察者模式。以下是一个简单的 JavaScript 实现:
// 创建一个事件管理器
const EventEmitter = {
events: {},
// 订阅事件
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
},
// 发布事件
publish(event, data) {
if (!this.events[event]) {
return;
}
this.events[event].forEach(callback => callback(data));
},
// 取消订阅事件
unsubscribe(event, callback) {
if (!this.events[event]) {
return;
}
const index = this.events[event].indexOf(callback);
if (index > -1) {
this.events[event].splice(index, 1);
}
}
};
// 创建两个订阅者
const subscriber1 = data => {
console.log('Subscriber 1:', data);
};
const subscriber2 = data => {
console.log('Subscriber 2:', data);
};
// 订阅事件
EventEmitter.subscribe('event1', subscriber1);
EventEmitter.subscribe('event1', subscriber2);
// 发布事件
EventEmitter.publish('event1', 'Hello, world!');
// 取消订阅事件
EventEmitter.unsubscribe('event1', subscriber1);
// 再次发布事件
EventEmitter.publish('event1', 'This event only has one subscriber now.');
上述代码中,EventEmitter
对象实现了订阅发布的功能。我们可以使用 subscribe
方法订阅一个事件,并传入一个回调函数作为事件的处理函数。使用 publish
方法发布一个事件,并将数据传递给订阅该事件的所有回调函数。使用 unsubscribe
方法可以取消订阅一个事件。
在实例代码中,我们创建了两个订阅者 subscriber1
和 subscriber2
,它们会分别输出接收到的数据。
输出结果为:
Subscriber 1: Hello, world!
Subscriber 2: Hello, world!
Subscriber 2: This event only has one subscriber now.
5、实现一个缓存函数
下面是一个简单的缓存函数的实现:
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = func.apply(this, args);
cache[key] = result;
return result;
};
}
使用示例:
function expensiveFunction(n) {
console.log('正在计算...');
return n * 2;
}
const memoized = memoize(expensiveFunction);
console.log(memoized(1)); // 输出: 正在计算... 2
console.log(memoized(1)); // 输出: 2 (从缓存中获取)
console.log(memoized(2)); // 输出: 正在计算... 4
console.log(memoized(2)); // 输出: 4 (从缓存中获取)
在上面的例子中,memoize
函数接收一个函数 func
作为参数,并返回一个新的函数。新函数使用一个对象 cache
作为缓存,用来存储每次函数调用的结果。
当调用新函数时,首先将传入的参数序列化为一个 JSON 字符串,作为键值 key
。如果 cache
中已经存在该键值,则直接将结果返回,否则调用原始的函数 func
,将其返回值存入 cache
中,并返回该值。这样,在后续调用相同参数的情况下,就可以直接从缓存中获取结果,避免重复计算。
这样,我们实现了一个简单的缓存函数。注意,以上实现只考虑了函数参数为基本类型的情况,如果函数参数包含引用类型,如对象或数组,需要使用深度克隆来生成唯一的键值。
6、数组去重
可以使用以下方法对 JavaScript 数组进行去重:
- 使用 Set 数据结构:Set 对象是一种新的数据结构,它允许存储任何类型的唯一值,并可以快速检索。我们可以通过将数组转换为 Set 对象,然后将 Set 对象转换回数组来实现数组去重。
var arr = [1, 2, 3, 3, 4, 4, 5];
var uniqueArr = Array.from(new Set(arr));
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 使用 Array.prototype.filter() 方法:filter() 方法创建一个新数组,其中包含通过指定函数测试的所有元素。我们可以使用 filter() 方法来检查每个元素是否在数组中已经存在,并创建一个只包含唯一元素的新数组。
var arr = [1, 2, 3, 3, 4, 4, 5];
var uniqueArr = arr.filter(function(value, index, arr) {
return arr.indexOf(value) === index;
});
console.log(uniqueArr); // [1, 2, 3, 4, 5]
- 使用 Array.prototype.reduce() 方法:reduce() 方法对数组中的每个元素依次执行指定的回调函数,并将回调的返回值累积到一个最终的返回值中。我们可以使用 reduce() 方法来创建一个只包含唯一元素的新数组,每次迭代时检查当前元素是否已经存在于累积数组中。
var arr = [1, 2, 3, 3, 4, 4, 5];
var uniqueArr = arr.reduce(function(acc, currentValue) {
if (acc.indexOf(currentValue) === -1) {
acc.push(currentValue);
}
return acc;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
这些方法都可以实现对 JavaScript 数组的去重,具体选择哪种方法取决于个人偏好和代码需求。
7、数组转树结构
8、实现接口最大并发
在JavaScript中,可以使用Promise对象和async/await语法来实现接口最大并发方法。下面是一个简单的示例代码:
// 模拟一个异步接口请求
function apiRequest(url) {
return new Promise(resolve => {
setTimeout(() => {
resolve('Response from ' + url);
}, Math.random() * 2000); // 模拟随机延迟
});
}
// 实现最大并发数的接口请求方法
async function apiRequests(urls, maxConcurrent) {
const results = [];
if (urls.length <= maxConcurrent) {
// 如果接口数量小于或等于最大并发数,直接发起请求
for (const url of urls) {
const result = await apiRequest(url);
results.push(result);
}
} else {
// 如果接口数量大于最大并发数,使用Promise.all控制最大并发数
let i = 0;
while (i < urls.length) {
const currentUrls = urls.slice(i, i + maxConcurrent);
const currentPromises = currentUrls.map(url => apiRequest(url));
const currentResults = await Promise.all(currentPromises);
results.push(...currentResults);
i += maxConcurrent;
}
}
return results;
}
// 测试代码
const urls = [
'https://api.example.com/1',
'https://api.example.com/2',
'https://api.example.com/3',
'https://api.example.com/4',
'https://api.example.com/5',
'https://api.example.com/6',
'https://api.example.com/7',
'https://api.example.com/8',
'https://api.example.com/9',
'https://api.example.com/10'
];
apiRequests(urls, 3)
.then(results => {
console.log(results);
})
.catch(error => {
console.error(error);
});
在这个例子中,apiRequest
函数模拟一个异步接口请求,使用setTimeout
方法模拟接口请求的延迟。apiRequests
函数是实现最大并发数的方法,它接受一个包含接口URL的数组和最大并发数作为参数。如果接口数量小于或等于最大并发数,就逐个发起请求并等待结果;如果接口数量大于最大并发数,就使用Promise.all
方法控制最大并发数,每次发起最大并发数个请求,并等待所有请求完成后再继续。最后,可以通过调用apiRequests
函数并传入接口URL数组和最大并发数来测试代码。
9、 防抖
const antiShake = (fn, waitTime) => {
let timer = null;
return (...args)=>{
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn(...args)
},waitTime)
}
}
10、节流
const throttle = (fn, wTime) => {
let timer = null;
return (...args) => {
if(timer) return
timer = setTimeout(()=>{
fn(...args)
timer = null;
},wTime)
}
}
11、reduce
12、深拷贝
13、promise
14、柯里化
柯里化(Currying)是一种将多个参数的函数转换为一系列只接受一个参数的函数的过程。下面是一个简单的 JavaScript 实现柯里化的示例:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
}
}
};
}
使用示例:
function add(a, b, c) {
return a + b + c;
}
var curriedAdd = curry(add);
console.log(curriedAdd(1, 2, 3)); // 输出 6
console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
这样,我们就可以通过 curry
函数将接受多个参数的函数转换为柯里化函数,然后可以灵活地传递参数,逐个调用生成的函数,直到所有参数都被传递并计算出结果。
15、千分位分隔符
可以使用以下的代码来实现千分位分隔符:
function formatNumber(number) {
// 使用正则表达式将数字转换成千分位格式
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 测试例子
console.log(formatNumber(123456789)); // 输出: "123,456,789"
console.log(formatNumber(9876543210)); // 输出: "9,876,543,210"
在此代码中,我们使用正则表达式/\B(?=(\d{3})+(?!\d))/g
将数字转换成千分位格式。其中,\B
表示非单词边界,(?=(\d{3})+(?!\d))
表示后面跟着至少三位数字,并且后面不跟着数字的位置。我们使用,
作为千分位的分隔符。
16、promisify
可以使用以下函数来实现 promisify 功能:
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
}
使用示例:
const fs = require('fs');
const readFile = promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
在上面的例子中,首先定义了一个 promisify
函数,它接受一个以回调函数形式的函数作为参数。它返回一个新的函数,该函数会返回一个 Promise 对象。
在新的函数中,我们调用原始函数,并传入相同的参数列表。但是,我们将回调函数替换为一个新的回调函数,该回调函数将根据原始函数的结果来解析或拒绝 Promise。如果原始函数返回错误,我们将调用 reject
方法,将错误传递给 Promise。如果原始函数返回结果,我们将调用 resolve
方法,将结果传递给 Promise。
最后,我们使用 promisify
函数来将 fs.readFile
方法封装成一个 Promise 形式的函数。然后,我们可以像使用 Promise 一样使用 readFile
函数,通过 then/catch 方法处理结果或错误。
17、js获取指定范围随机数
使用 window.crypto.getRandomValues() 获取数据数
//获取指定范围
function getRandom(min: any, max: any) {
const randomBuffer = new Uint32Array(1);
window.crypto.getRandomValues(randomBuffer);
let randomNumber = randomBuffer[0] / (0xffffffff + 1);
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(randomNumber * (max - min + 1)) + min;
}
18、vue3 获取本地图片 方法封装
// ../../../assets 图片本地路径
const imgUrl = (path: string) => {
return new URL(`../../../assets/${path}`, import.meta.url).href
}