JS面试④
1.谈一谈浏览器的缓存机制?
浏览器的缓存机制指的是通过在一段时间内保留已接收到的 web 资源的一个副本,如果在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。使用 web 缓存可以有效地提高页面的打开速度,减少不必要的网络带宽的消耗。
web 资源的缓存策略一般由服务器来指定,可以分为两种,分别是强缓存策略和协商缓存策略。
1.1使用强缓存策略时
如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http
头信息中的 Expires
属性和 Cache-Control
属性。
服务器通过在响应头中添加 Expires
属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
Expires
是 http1.0 中的方式,因为它的一些缺点,在 http 1.1 中提出了一个新的头部属性就是 Cache-Control
属性,
它提供了对资源的缓存的更精确的控制。它有很多不同的值,常用的比如我们可以通过设置 max-age
来指定资源能够被缓存的时间
的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过期的时间,因此相对于Expires
来说,这种方式更加有效一些。常用的还有比如 private
,用来规定资源只能被客户端缓存,不能够代理服务器所缓存。还有如 no-store
,用来指定资源不能够被缓存,no-cache
代表该资源能够被缓存,但是立即失效,每次都需要向服务器发起请求。
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control
的优先级要高于 Expires
。
1.2使用协商缓存策略时
会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。协商缓存也可以通过两种方式来设置,分别是 http
头信息中的 Etag
和 Last-Modified
属性。
服务器通过在响应头中添加 Last-Modified
属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since
的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified
标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified
却没有改变,
这样会造成缓存命中的不准确。
因为 Last-Modified
的这种可能发生的不准确性,http
中提供了另外一种方式,那就是 Etag
属性。服务器在返回资源的时候,在头信息中添加了 Etag
属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match
属性,这个属性的值就是上次返回的资源的 Etag
的值。服务接收到请求后会根据这个值来和资源当前的 Etag
的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified
的方式更加精确。
当 Last-Modified
和 Etag
属性同时出现的时候,Etag
的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag
的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag
属性。
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。
它们缓存不命中时,都会向服务器发送请求来获取资源。
在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。
浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
详细资料可以参考:
《浅谈浏览器缓存》
《前端优化:浏览器缓存技术介绍》
《请求头中的 Cache-Control》
《Cache-Control 字段值详解》
2. Ajax 是什么? 如何创建一个 Ajax?
相关知识点:
2005 年 2 月,AJAX 这个词第一次正式提出,它是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的
异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
具体来说,AJAX 包括以下几个步骤。
- 1.创建 XMLHttpRequest 对象,也就是创建一个异步调用对象
- 2.创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息
- 3.设置响应 HTTP 请求状态变化的函数
- 4.发送 HTTP 请求
- 5.获取异步调用返回的数据
- 6.使用 JavaScript 和 DOM 实现局部刷新
一般实现:
//1.创建xhr对象
let xhr=new XMLHttpRequest();
//2.准备发送 open(method: string, url: string, async: boolean)
xhr.open('请求方式','URL路径','是否异步');
//get请求时,在URL路径上传递参数,添加`?name=${name}&pwd=${pwd}`
//post请求时,参数在send中传递
//3.发送 send()
//配置请求头
//xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded;charset=utf-8");//post请求
//存储变量
//let data=`name=${name}&password=${pwd}`;
//发送变量
//xhr.send(data);
xhr.send();
//4.监听函数--监听当前Ajax请求进行到 哪一步
xhr.onreadystatechange=function(){
// 状态改变
if(xhr.readyState === 4){//响应数据
if(xhr.status === 200){//http协议状态 200成功 404资源不存在
console.log(xhr.responseText);//返回的数据字符串格式
console.log(xhr.response); //返回响应数据
}
}
3. 同步和异步的区别?
相关知识点:
同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是处于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。
异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
回答:
同步指的是当一个进程在执行某个请求的时候,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返
回为止再继续向下执行。
异步指的是当一个进程在执行某个请求的时候,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等
待消息的返回,当消息返回时系统再通知进程进行处理。
详细资料可以参考:
《同步和异步的区别》
4. 什么是浏览器的同源政策?
同源的指的是两个域的协议、域名、端口号必须相同,否则则不属于同一个域。
我对浏览器的同源政策的理解是,一个域下的 js 脚本在未经允许的情况下,不能够访问另一个域的内容。这里的同源的指的是两个
域的协议、域名、端口号必须相同,否则则不属于同一个域。
同源政策主要限制了三个方面
第一个是当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
第二个是当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。
第三个是当前域下 ajax 无法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者
script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
5. 如何解决跨域问题?
相关知识点:
-
- 通过 jsonp 跨域
-
- 跨域资源共享(CORS)
-
- nodejs 中间件代理跨域
-
- WebSocket 协议跨域
回答:
(1)使用 jsonp 来实现跨域请求,它的主要原理是通过动态构建 script 标签来实现跨域请求,因为浏览器对 script 标签的引入没有跨域的访问限制 。通过在请求的 url 后指定一个回调函数,然后服务器在返回数据的时候,构建一个 json 数据的包装,这个包装就是回调函数,然后返回给前端,前端接收到数据后,因为请求的是脚本文件,所以会直接执行,这样我们先前定义好的回调函数就可以被调用,从而实现了跨域请求的处理。这种方式只能用于 get 请求。
(2)使用 CORS 的方式,CORS 是一个 W3C 标准,全称是"跨域资源共享"。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,因此我们只需要在服务器端配置就行。浏览器将 CORS 请求分成两类:简单请求和非简单请求。对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是会在头信息之中,增加一个 Origin 字段。Origin 字段用来说明本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。对于如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,ajax 不会收到响应信息。如果成功的话会包含一些以 Access-Control- 开头的字段。
非简单请求,浏览器会先发出一次预检请求,来判断该域名是否在服务器的白名单中,如果收到肯定回复后才会发起请求。
(3)使用 websocket 协议,这个协议没有同源限制。
(4)使用服务器来代理跨域的访问请求,就是有跨域的请求操作时发送请求给后端,让后端代为请求,然后最后将获取的结果发返回。(`http-proxy-middelware`,或proxy代理)
详细资料可以参考:
《前端常见跨域解决方案(全)》
《浏览器同源政策及其规避方法》
《跨域,你需要知道的全在这里》
《为什么 form 表单提交没有跨域问题,但 ajax 提交有跨域问题?》
6. 简单谈一下 cookie ?
我的理解是 cookie 是服务器提供的一种用于维护会话状态信息的数据,通过服务器发送到浏览器,浏览器保存在本地,当下一次有同源的请求时,将保存的 cookie 值添加到请求头部,发送给服务端。这可以用来实现记录用户登录状态等功能。cookie 一般可以存储 4k 大小的数据,并且只能够被同源的网页所共享访问。
服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条cookie 包括了9个属性值 name、value、expires、domain、path、secure、HttpOnly、SameSite、Priority。其中 name 和 value 分别是 cookie 的名字和值。expires 指定了 cookie 失效的时间,domain 是域名、path是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。SameSite 属性用来限制第三方 cookie,可以有效防止 CSRF 攻击,从而减少安全风险。Priority 是 chrome 的提案,定义了三种优先级,当 cookie 数量超出时低优先级的 cookie 会被优先清除。
在发生 xhr 的跨域请求的时候,即使是同源下的 cookie,也不会被自动添加到请求头部,除非显示地规定。
详细资料可以参考:
《HTTP cookies》
《聊一聊 cookie》
7.call() 和 .apply() 的区别?
它们的作用一模一样,区别仅在于传入参数的形式的不同。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
详细资料可以参考:
《apply、call 的区别和用途》
8. JavaScript 类数组对象的定义?
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。
常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length
属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
详细的资料可以参考:
《JavaScript 深入之类数组对象与 arguments》
《javascript 类数组》
《深入理解 JavaScript 类数组》
9. 简单介绍一下 V8 引擎的垃圾回收机制
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步:
(1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
(2)如果对象不存活,则释放对象的空间。
(3)最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
详细资料可以参考:
《深入理解 V8 的垃圾回收原理》
《JavaScript 中的垃圾回收》
10. 哪些操作会造成内存泄漏?
相关知识点:
- 1.意外的全局变量
- 2.被遗忘的计时器或回调函数
- 3.脱离 DOM 的引用
- 4.闭包
回答:
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留
在内存中,而无法被回收。
第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回
收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
详细资料可以参考:
《JavaScript 内存泄漏教程》
《4 类 JavaScript 内存泄漏及如何避免》
《杜绝 js 中四种内存泄漏类型的发生》
《javascript 典型内存泄漏及 chrome 的排查方法》
11. 如何判断当前脚本运行在浏览器还是 node 环境中?(阿里)
typeof window === 'undefined' ? 'node' : 'browser';
通过判断当前环境的 window 对象类型是否为 undefined,如果是undefined,则说明当前脚本运行在node环境,否则说明运行在window环境。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步