基础知识积累

一、从输入url地址栏到所有内容显示在界面上发生了什么?

  • 1、浏览器向dns服务器请求解析该URL中的域名所对应的IP地址
  • 2、建立TCP链接(三次握手)
  • 3、浏览器发出读取文件(URL中域名后面部分对应的文件)的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器
  • 4、服务器对浏览器请求作出响应,并把对应的html文本发送给浏览器
  • 5、浏览器将该html文本显示内容
  • 6、释放TCP连接(四次挥手)

二、TCP的三次握手和四次挥手

1
2
3
4
5
6
ack --- 确认号码
seq --- 顺序号码
ISN --- 初始序列号
ACK --- 确认,使得确认号有效(握手使用)
SYN --- 用于初始化一个连接的序列号,建立联机(同步序列编号)
FIN --- 该报文的发送方已经结束向对方发送数据

三次握手

  • 第一次握手(SYN=1,ACK=0,seq=x)

Client将标志位SYN置为1,随机产生一个值seq=x,并将该数据包发送给Server,Client进入SYN_SENT状态,等 待Server确认。

  • 第二次握手(SYN=1, ACK=1, seq=y, ack=x+1)

Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:(ACK=1,seq=x+1,ack=y+1)

Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并将该数据包发送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

TCP四次挥手

  • 第一次挥手(FIN=1,seq=u)

Client 想要关闭连接,Client 会发送一个FIN标志位置为1,当前序列号为u的包,表示需要关闭连接了。Client进入 FIN_WAIT_1 状态。

  • 第二次挥手(ACK=1,seq=v,ack=u+1)

Server收到Client的FIN包之后,会发送一个确认序号为收到的序列号u+1的包,表明自己接受到了Client关闭连接的请求,但还未准备好关闭连接。Server进入 CLOSE_WAIT 状态,Client进入 FIN_WAIT_2 状态。

  • 第三次挥手(FIN=1,ACK=1,seq=w,ack=u+1)

当Server将剩余数据发送完之后,会发送一个自己的FIN包,序列号为u+1。Server进入 LAST_ACK 状态,等待来自Client的最后一个ACK。

  • 第四次挥手(ACK=1,seq=u+1,ack=w+1)

Client接收到来自Server端的关闭请求之后,发送最后一个ACK确认包,确认序号设置为收到序号加1。Client进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。Server接收到这个确认包之后,关闭连接,进入CLOSED状态。(Client会等待2MSL之后,没有收到Server的ACK ,就确认Server进入CLOSED状态,自己也关闭进入CLOSED状态。)

三、for in 与 for of 的区别

  • for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合,得到的是value值,for of不能对象用
  • for in遍历对象得到key值,遍历数组得到下标

四、九种跨域方式实现原理

严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。

同源策略:

所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了

跨域解决方案

1.jsonp优点是简单兼容性好,缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击

原理:利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的JSON数据,JSONP请求一定需要对方的服务器做支持才可以。

实现jsonp函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
window[callback] = data => {
resolve(data)
document.body.removeChild(script)
}
})
}

jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'ILoveYou' },
callback: 'show'
}).then(data => {
console.log(data)
})

服务端实现

1
2
3
4
5
6
7
8
9
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

Jquery 的jsonp形式

1
2
3
4
5
6
7
8
9
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});

2.cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现

浏览器如果支持会自动进行CORS通信,实现的关键在于后端

服务端设置Access-Control-Allow-Origin就开启了CORS,该属性表示哪些域名可以访问资源,设置通配符表示所有都可以访问。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求:

条件1:使用下列方法之一:GET/HEAD/POST

条件2:Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

复杂请求:

不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

3.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递

  • 多窗口之间消息传递

  • 页面与嵌套的iframe消息传递

  • 上面三个场景的跨域数据传递

    postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

1
otherWindow.postMessage(message, targetOrigin, [transfer]);
1
2
3
4
5
6
7
8
9
10
11
12
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
</script>
1
2
3
4
5
// b.html
window.onmessage = function(e) {
console.log(e.data) //我爱你
e.source.postMessage('我不爱你', e.origin)
}

4.websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

使用Socket.io

1
2
3
4
5
6
7
8
9
10
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
</script>
1
2
3
4
5
6
7
8
9
10
11
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('我不爱你')
});
})

5.Node中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。

实现一个node服务作为中间件,浏览器访问node 代理服务器,node代理服务器转发请求给目标服务器,目标服务器返回响应给node服务,node服务再转发响应给浏览器

6.nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

7.window.name + iframe

通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

8.location.hash + iframe

a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

9.document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

五、http和https的区别及优缺点

  • http 是超文本传输协议,信息是明文传输,HTTPS 协议要比 http 协议安全,https 是具有安全性的 ssl 加密传输协议,可防止数据在传输过程中被窃取、改变,确保数据的完整性。
  • http 协议的默认端口为 80,https 的默认端口为 443。
  • http 的连接很简单,是无状态的。https 握手阶段比较费时,会使页面加载时间延长 50%,增加 10%~20%的耗电。
  • https 缓存不如 http 高效,会增加数据开销。
  • Https 协议需要 ca 证书,费用较高,功能越强大的证书费用越高。
  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名,IPV4 资源支持不了这种消耗。

六、Cookie、sessionStorage、localStorage 的区别

相同点:

  • 都是客户端缓存

不同点

  • cookie数据大小不能超过4k;sessionStorage和localStorage的存储比cookie大得多,可以达到5M+
  • cookie设置的过期时间之前一直有效;localStorage永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除
  • cookie的数据会自动的传递到服务器;sessionStorage和localStorage数据保存在本地

七、浏览器重绘和重排的区别?

  • 重排/回流(Reflow):当DOM的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素。
  • 重绘(Repaint): 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变

『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。

浏览器缓存原理

强缓存:Cache-Control优先于Expires, 协商缓存:ETag优先于Last-Modified
总结:
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存

八、进程、线程和协程

进程: 是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
线程: 是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成
协程: 是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。

九、HTML5 新特性、语义化

HTML5的语义化指的是合理正确的使用语义化的标签来创建页面结构

语义化标签
header nav section main article aside footer
语义化的优点

  • 在没CSS样式的情况下,页面整体也会呈现很好的结构效果
  • 代码结构清晰,易于阅读
  • 利于开发和维护
  • 有利于搜索引擎优化(SEO)

十、盒子模型

  • 标准盒子模型:width是指content部分的高度
  • ie盒子模型:width表示 content + padding + border 三个部分组成

十一、BFC(块级格式化上下文)

BFC是CSS布局的一个概念,是一个独立的渲染区域,规定了内部box如何布局, 并且这个区域的子元素不会影响到外面的元素,其中比较重要的布局规则有内部 box 垂直放置,计算 BFC 的高度的时候,浮动元素也参与计算。

如何创建bfc

  • 根元素,即HTML元素
  • float的值不为none
  • position 为absolute或fixed
  • display的值为inline-block、table-cell、table-caption
  • overflow的值不为visible

十二、JS中数据类型及区别

  • 基本数据类型: Number、String、Boolean、 undefined、null、 Symbol, 在内存中占据固定大小,保存在栈内存中
  • 引用数据类型: Object(对象)、Function(函数),其他还有Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等 引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。

十三、JS中的数据类型检测方案

  • 1、typeof
    能快速区分基本数据类型,不能区分Object、Array、Null,都返回object
  • 2、instanceof
    能够区分Array、Object和Function,适合用于判断自定义的类实例对象,Number,Boolean,String基本数据类型不能判断
  • 3、Object.prototype.toString.call()
    精准判断数据类型

十四、var && let && const

三者的区别

  • 1、var定义变量,没有块的概念,可以跨块访问,但不能跨函数访问。let定义变量,只能在块作用域中访问,不能跨块访问,也不能跨函数访问。const用于定义常量,使用时候必须初始化(必须赋值),只能在块作用域中访问,且不能修改
  • 2、var可以先试用后声明,因为存在变量提升;let必须先声明后使用
  • 3、var是允许在相同作用域内重复声明同一个变量的,而let与const不允许这一现象。
  • 4、在全局上下文中,基于let声明的全局变量和全局对象globel(window)没有任何关系 ; var声明的变量会和globel有映射关系;
  • 5、会产生暂时性死区
  • 6、let /const/function会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于let的块作用特征即可解决

十五、JS垃圾回收机制

1、项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。
浏览器垃圾回收机制/内存回收机制:
浏览器的Javascript具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

  • 标记清除:
  • 谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
  • IE浏览器:“引用计数法”,当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。
    2、内存泄漏
    在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器

十六、作用域和作用域链

定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成

  • 1、全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。
  • 2、函数作用域:在固定的代码片段才能被访问
    作用:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
    一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

十七、闭包的两大作用:保存/保护

  • 闭包是一个可以访问外部作用域中变量的内部函数。
  • 这些被引用的变量直到闭包被销毁时才会被销毁。
  • 闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易。
  • 可以通过闭包来达到封装性。

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);
(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

闭包形成的条件:

  • 1、函数的嵌套
  • 2、内部函数引用外部函数的局部变量,延长外部函数的变量生命周期

闭包的用途:

  • 1、模仿块级作用域
  • 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
  • 封装私有化变量
  • 创建模块

闭包的优点:延长局部变量的生命周期
闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

十八、JS 中 this 的五种情况

  • 1、作为普通函数执行时,非严格模式(window)、严格模式(undefined)
  • 2、当函数作为对象的方法被调用时,this就会指向这个对象
  • 3、构造器调用,this指向返回的这个对象
  • 4、箭头函数的this绑定看的是this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。
  • 5、基于Function.prototype上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表, bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new 时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。

十九、原型 && 原型链

在规范里,prototype 被定义为:给其它对象提供共享属性的对象。

JS是通过函数来模拟类,当我们创建一个函数的时候,js会给这个函数自动添加prototype属性,值是一个包含constructor属性的对象,不是空对象。当我们把函数当成构造函数通过new关键词调用的时候,js会帮我们创建该构造函数的实例,实例继承构造函数prototype的所以属性和方法(实例通过设置自己的proto指向构造函数的prototype来实现继承)

原型链的概念,仅仅是在原型这个概念基础上所作的直接推论。
既然 prototype 只是恰好作为另一个对象的隐式引用的普通对象。那么,它也是对象,也符合一个对象的基本特征。 也就是说,prototype 对象也有自己的隐式引用,有自己的 prototype 对象。
如此,构成了对象的原型的原型的原型的链条,直到某个对象的隐式引用为 null,整个链条终止。

二十、new运算符的实现机制

1、创建一个新的空对象
2、设置新对象的隐式原型(proto)为构造函数的显式原型(prototype)
3、使用apply改变构造函数this指向新的对象
4、根据函数apply的返回值,判断返回值,如果值是一个对象,则返回这个对象,若不是,则返回这个新创建的对象。

手写实现new

1
2
3
4
5
6
7
function objectFactory() {
const obj = new Object()
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
const result = Constructor.apply(obj, arguments)
return typeof result === 'object' ? result : obj
}

二一、EventLoop 事件循环

js是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以将同步代码压入执行栈中,依次执行,将异步代码推入异步队列中,异步队列分为宏任务和微任务队列。微任务队列优先于宏任务队列,微任务的代表:Promise.then,MutationObserver,宏任务的代表:setImmediate setTimeout setInterval。

浏览器的事件循环
事件环的运行机制是:先会执行执行栈中的内容,然后执行当前上下文的微任务队列,微任务队列执行完毕后,再执行宏任务,然后执行其中的同步代码。如此反复,这样就形成一个循环。这个过程被称为事件循环(event loop)

事件循环可以简单的描述为以下四个步骤:

  • 1、函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
  • 2、此期间WebAPIs完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
  • 3、执行栈为空时,Event Loop把微任务队列执行清空;
  • 4、微任务队列清空后,进入宏任务队列,取队列的第一项任务放入Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。

node环境的事件环
执行顺序如下:

  • timers: 计时器,执行setTimeout和setInterval的回调
  • pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare: 队列的移动,仅系统内部使用
  • poll轮询: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  • check: 执行setImmediate回调,setImmediate在这里执行
  • close callbacks: 执行close事件的callback,一些关闭的回调函数,如:socket.on(‘close’, …)

二二、setTimeout、Promise、Async/Await 的区别

  • setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行
  • Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。
  • async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

二三、介绍节流防抖原理、区别以及应用

节流: 事件触发后,在一段时间内只会触发一次,包括时间戳版和定时器版

1
2
3
4
5
6
7
8
9
10
11
//时间戳版
function throttle(fn, wait) {
let prev = 0
return function() {
let now = new Date()
if (now - prev > wait) {
fn.apply(this, arguments)
prev = now
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//定时器版
function throttle(fn, wait) {
let timer = null
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, wait)
}
}
}

防抖:事件触发后,在一段时间后才会执行函数,如果在这段时间内再次触发,会重新计算函数的执行时间,分为非立即执行和立即执行两个版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 非立即执行版
function debounce(fn, wait) {
let timer = null
return function() {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait)
}
}

// 立即执行版
function debounce(fn, wait) {
let timer = null
return function() {
const now = !timer
timer && clearTimeout(timer)
timer = setTimeout(() => {
timer = null
}, wait)
if (now) {
fn.apply(this, arguments)
}
}
}

使用场景:

  • 节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
  • 索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染