16、 什么是作用域链?
- 作用域可以视为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域根据标识符名称进行变量查找。
- 简单来说作用域就是变量的有效范围。在一定的空间里可以对变量数据进行读写操作,这个空间就是变量的作用域。
作用域链的本质,就是底层变量查找机制
过程: 当在js
中使用一个变量的时候,首先js
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
作用: 保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
17、 浏览器的垃圾回收机制
(1)内存的生命周期
JS 环境中分配的内存, 一般有如下生命周期:
-
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
-
内存使用:即读写内存,也就是使用变量、函数等
-
内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
全局变量一般不会回收, 一般局部变量的的值, 不用了, 会被自动回收掉
(2)垃圾回收的概念
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
(3)垃圾回收的方式
1.引用计数法
- 这个用的相对较少,IE采用的引用计数算法。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
- 这种方法会引起循环引用 的问题:例如:
obj1
和obj2
通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1
和obj2
还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
2.标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
- 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量"进入环境",被标记为"进入环境"的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为"离开环境",被标记为"离开环境"的变量会被内存释放。
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
(4)如何减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 - 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
(5)内存泄漏是什么
是指由于疏忽或错误造成程序未能释放已经不再使用的内存
(6)哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
18、什么是函数式编程、
主要的编程范式有三种:命令式编程,声明式编程和函数式编程
函数式编程强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程
优点
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
- 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
- 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
- 隐性好处。减少代码量,提高维护性
缺点
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
- 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作
19、 事件循环Event Loop
事件循环指的是js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。
(1) JS的执行机制(同步任务、异步任务)
单线程是为了避免UI操作混乱,所有和UI操作相关的开发语言都应该是单线程。
JS是一门单线程语言,单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行下一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的觉。为了解决这个问题,JS中出现了同步和异步。
同步任务:即主线程上的任务,按照顺序由上⾄下依次执⾏,当前⼀个任务执⾏完毕后,才能执⾏下⼀个任务。
异步任务:不进⼊主线程,⽽是进⼊任务队列的任务,执行完毕之后会产生一个回调函数,并且通知主线程。当主线程上的任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。
(2) 宏任务与微任务的概念与区别
为了让任务有条不紊地在主线程上执行,页面进程引入了 消息队列 和 事件循环机制 ,渲染进程内部也会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for
循环,不断地从这些任务队列中取出任务并执行任务。这些消息队列中的任务就称为 宏任务。
微任务 是一个需要异步执行的回调函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。当 JS 执行一段脚本(一个宏任务)的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个 微任务队列 。也就是说 每个宏任务都关联了一个微任务队列。
(3) 事件循环Event Loop执行机制
1.进入到script标签,就进入到了第一次事件循环.
2.遇到同步代码,立即执行
3.遇到宏任务,放入到宏任务队列里.
4.遇到微任务,放入到微任务队列里.
5.执行完所有同步代码
6.执行微任务代码
7.微任务代码执行完毕,本次队列清空
8.寻找下一个宏任务,重复步骤1
(4) 常见的宏任务与微任务分别有哪些
常见的宏任务有:
- setTimeout 函数:用于在指定的时间后执行一个函数。
- setInterval 函数:用于按照指定的时间间隔重复执行一个函数。
- requestAnimationFrame 函数:用于在浏览器的每一帧中执行一个函数。
- I/O 操作:例如文件读取、网络请求等。
- UI 事件:例如鼠标点击、键盘按下等。
常见的微任务有:
- Promise 对象的回调函数:当 Promise 对象的状态变为 resolved 或 rejected 时,会调用相应的回调函数。
- MutationObserver 的回调函数:当监听的 DOM 元素发生变化时,会调用相应的回调函数。
- Async/Await 中的异步函数:Async/Await 是基于 Promise 的语法糖,异步函数内部的代码会在微任务队列中执行。
- process.nextTick 函数(Node.js 环境):用于在当前事件循环的下一个滴答中执行一个函数。
20、 浏览器缓存机制
缓存行为
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
缓存位置
Service Worker
:是运行在浏览器背后的独立线程,无法直接访问DOM
,但可以用来做离线缓存
、消息推送
和网络代理
。传输协议必须为HTTPS
。Memory Cache
:内存中的缓存Disk Cache
:存储在硬盘中的缓存Push Cache
:(推送缓存
)是HTTP/2
中的内容;
缓存过程
-
浏览器第一次加载资源,服务器返回
200
,浏览器将资源文件从服务器上请求下载下来,并把response header
及该请求的返回时间一并缓存; -
下一次加载资源时,先比较当前时间和上一次返回
200
时的时间差,如果没有超过cache-control
设置的max-age
,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1
,则用expires
判断是否过期);如果时间过期,则向服务器发送header
带有If-None-Match
和If-Modified-Since
的请求; -
服务器收到请求后,优先根据
Etag
的值判断被请求的文件有没有做修改,Etag
值一致则没有修改,命中协商缓存,返回304
;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200
; -
如果服务器收到的请求没有
Etag
值,则将If-Modified-Since
和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304
;不一致则返回新的last-modified
和文件并返回200
;
强缓存
强缓存表示在缓存期间不需要发送请求。
强缓存
通过设置两种 HTTP Header
来实现,分别是Expires
和 Cache-Control
。
Expires
是HTTP/1.0
的产物。值代表的是服务端的时间,并且Expires
受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Cache-Control
出现于HTTP/1.1
,优先级高于Expires
。该属性值表示资源会在多少秒
后过期,需要再次请求。其中的max-age
指令,记录了缓存过期的相对时间(相对请求的时间)。
注意:
如果
Cache-Control
属性值为 no-store ,表示不进行任何缓存;属性值为 no-cache ,表示强制使用协商缓存。
协商缓存
如果是首次请求
或者Cache-Control
的属性设置为 no-cache
时,又或者如果缓存过期了
,都会向服务器发送请求,并且请求头中携带If-Modified-Since
和 If-None-Match
来判断是否命中协商缓存,如果命中,则返回304
状态码并且更新浏览器缓存有效期。
字段 | Header类型 | 协议版本 | 缓存类型 |
---|---|---|---|
Last-Modified | Response(响应头) | HTTP1.0 | 协商缓存 |
If-Modified-Since | Request(请求头) | HTTP1.0 | 协商缓存 |
ETag | Response(响应头) | HTTP1.1 | 协商缓存 |
If-None-Match | Request(请求头) | HTTP1.1 | 协商缓存 |
-
Last-Modified
表示本地文件最后修改时间,发送请求时,会将当前的Last-Modified
值作为If-Modified-Since
这个字段的内容,放在请求头中发送给服务器,去询问服务器在该时间后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回304
状态码。 -
ETag
类似于文件指纹,请求时会将当前ETag
作为If-None-Match
这个字段的内容,并放到请求头中发送给服务器,服务器接收到If-None-Match
后,会跟服务器上该资源的ETag
进行比对,有变动的话就将新的资源发送回来,否则返回304
状态码。