js面试题(二)
1.几个短而让我印象深刻的题
if (!("a" in window)) { var a = 10; //var命令在块级作用域就是在全局的,let才在块级作用域生效 } console.log(a); // undefined
(function(){ var a = b = 3; })() console.log(typeof a === "undefined"); // true console.log(typeof b === "undefined"); // false // 这里涉及的就是立即执行和闭包的问题,还有变量提升,运算符执行方向(=号自左向右) // 那个函数可以拆成这样 (function(){ var a=3; /* 局部变量,外部没法访问*/ b = 3; /* 全局变量, window.b === 3 , 外部可以访问到*/ })() // 若是改成这样,这道题应该是对的 console.log(typeof b === "number" && b ===3 ); // true
function foo(something){ this.a = something; } var obj1 = { foo:foo }; var obj2 = {}; obj1.foo(2) console.log(obj1.a) // 2 ,此时的 this 上下文还在 obj1内,函数作为对象的属性调用,this就是谁调用是谁 obj1.foo.call(obj2,3); // 用 call 强行改变上下文为 obj2内 console.log(obj2.a); // 3 var bar = new obj1.foo(4); // 这里产生了一个实例 console.log(obj1.a); // 2 console.log(bar.a); // 4; new的绑定比隐式和显式绑定优先级更高
function fn() { alert(a); var a = 200; alert(a); } fn(); // undefined / 200 ; 涉及变量提升 alert(a); // undefined var a; alert(a); // undefined var a = 300; alert(a); // 300
var obj1= { name:'obj1', fn:function(){ console.log(this.name); } }; var obj2 = {name:'obj2'}; var obj3 = {name:'obj3'}; // 这道题主要涉及的是 this 指向的问题.. obj1.fn(); // obj1 var newFn = obj1.fn; newFn(); // undefined, this 指向 window newFn.call(obj2);// obj2, this 指向 obj2 obj3.fn = newFn; /* fn (){ console.log(this.name); } */ obj3.fn(); // 这里指向的是 obj3 .所以输出 obj3
// 这道题来作为笔试题很绕,因为要回答的答案很多(脑海构思)..反正我是遇到了.. // 这道题主要考核的是对原型链继承这块的理解 function Parent(){ this.a = 1; this.b = [1,2,this.a]; this.c = {demo:5}; this.show = function(){ console.log(this.a + '' + this.c.demo + ':' + this.b) } } function Child(){ this.a = 2; this.change = function(){ this.b.push(this.a); this.a = this.b.length; this.c.demo = this.a++; } } Child.prototype = new Parent(); var parent = new Parent(); var child1 = new Child(); var child2 = new Child(); child1.a = 11; child2.a = 12; // 这前面几个还算简单,继续看下去 parent.show(); // 15:1,2,1 // 因为 Child 自身没有 show 的方法,所以往原型链的上游找; // 找到父类的,this 因为没更改,所以输出结果如下,this谁调用就是谁,a本身有 child1.show(); // 115:1,2,1 child2.show(); // 125:1,2,1 child1.change(); // 改变一些数据,没有输出 child2.change(); // +1 parent.show(); // 15:1,2,1 child1.show(); // 55:1,2,1,11,12 child2.show(); // 65:1,2,1,11,12
2.任务队列
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,形成一个执行栈,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
宏任务 & 微任务
这里需要注意的是new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务
- 宏任务(macro-task):整体代码script、setTimeOut、setInterval
- 微任务(mincro-task):promise.then、promise.nextTick(node)
EventLoop 事件循环
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
- 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
- 同步任务会直接进入主线程依次执行;
- 异步任务会再分为宏任务和微任务;
- 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
- 上述过程会不断重复,这就是Event Loop事件循环;
3.哪些是异步任务
- setTimeout 和 setInterval
- DOM 事件
- Promise
- 网络请求
- I/O
4.常考题
第一题
下面代码的输出结果是什么?
console.log(1); setTimeout(function(){ console.log(2) }, 0); console.log(3)
答案:1,3,2
解析: console.log()
是同步任务, setTimeout
是异步任务。异步任务会等同步任务执行完再执行。虽然 setTimeout
设置的延迟是 0,但浏览器规定延迟最小为 4ms,所以 console.log(2)
在 4ms 后被放入任务队列。当同步任务执行完,即打印完 1,3 之后,主线程再从任务队列中取任务,打印 2。
2) 第二题
下面的代码输出结果是什么
console.log('A') while(true){} console.log('B')
答案:A
解析:代码从上往下执行,先打印 A,然后 while 循环,因为条件一直是 true,所以会进入死循环。while 不执行完就不会执行到第三行。
这个题目还有个变种:
console.log('A'); setTimeout(function(){ console.log('B') }, 0); while(1){}
同样只会输出 A。因为异步任务需要等同步任务执行完之后才执行,while 进入了死循环,所以不会打印 B。
3) 第三题
下面代码输出结果
for(var i=0; i<4; i++){ setTimeout(function(){ console.log(i) }, 0) }
结果:4个4。
解析:这题主要考察异步任务放入任务队列的时机。当执行到 setTimeout
即定时器时,并不会马上把这个异步任务放入任务队列,而是等时间到了之后才放入。然后等执行栈中的同步任务执行完毕后,再从任务队列中依次取出任务执行。
for 循环是同步任务,会先执行完循环,此时 i 的值是 4,var是全局变量,4ms后 console.log(i)
被依次放入任务队列,此时如果执行栈中没有同步任务了,就从任务队列中依次取出任务,所以打印出 4 个 4。
那么如何才能按照期望打印出 0, 1,2,3 呢?有2个方法:
//方法1:把 var 换成 let,let指令在块作用域中有效,在这里,let本质上就是形成了一个闭包 for (let i = 0; i < 4; i++) { setTimeout(function () { console.log(i); }, 0); } console.log(i);//i is not defined //方法2:使用立即执行函数 // 利用立即执行函数,当for循环执行时,就会立即执行setTimeout,从而使得到的每个副本i值都不一样,利用了闭包 for (let i = 0; i < 4; i++) { (function (i) { setTimeout(function () { console.log(i); }, 0); })(i); }
//方法3:加闭包 for (let i = 0; i < 4; i++) { var a = function () { var j = i; setTimeout(function () { console.log(j); }, 0); }; a(); }
4) 第四题
setTimeout(function(){ console.log(1) }); new Promise(function(resolve){ console.log(2); for(var i = 0; i < 10000; i++){ i == 9999 && resolve(); } }).then(function(){ console.log(3) }); console.log(4); 执行结果: // 2, 4, 3, 1
分析:
1.setTimeout是异步,且是宏函数,放到宏函数队列中;
2.new Promise是同步任务,直接执行,打印2,并执行for循环;
3.promise.then是微任务,放到微任务队列中;
4.console.log(4)同步任务,直接执行,打印4;
5.此时主线程任务执行完毕,检查微任务队列中,有promise.then,执行微任务,打印3;
6.微任务执行完毕,第一次循环结束;从宏任务队列中取出第一个宏任务到主线程执行,打印1;
7.结果:2,4,3,1
let a = new Promise( function(resolve, reject) { console.log(1) setTimeout(() => console.log(2), 0) console.log(3) console.log(4) resolve(true) } ) a.then(v => { console.log(8) }) let b = new Promise( function() { console.log(5) setTimeout(() => console.log(6), 0) } ) console.log(7)
最终输出结果就是:1、3、4、5、7、8、2、6。异步队列排列按顺序来的
6) 第六题
function add(x, y) { console.log(1) setTimeout(function() { // timer1 console.log(2) }, 1000) } add(); setTimeout(function() { // timer2 console.log(3) }) new Promise(function(resolve) { console.log(4) setTimeout(function() { // timer3 console.log(5) }, 100) for(var i = 0; i < 100; i++) { i == 99 && resolve() } }).then(function() { setTimeout(function() { // timer4 console.log(6) }, 0) console.log(7) }) console.log(8) 执行结果 //1,4,8,7,3,6,5,2
分析:
1.add()是同步任务,直接执行,打印1;
2.add()里面的setTimeout是异步任务且宏函数,记做timer1放到宏函数队列;
3.add()下面的setTimeout是异步任务且宏函数,记做timer2放到宏函数队列;
4.new Promise是同步任务,直接执行,打印4;
5.Promise里面的setTimeout是异步任务且宏函数,记做timer3放到宏函数队列;
6.Promise里面的for循环,同步任务,执行代码;
7.Promise.then是微任务,放到微任务队列;
8.console.log(8)是同步任务,直接执行,打印8;
9.此时主线程任务执行完毕,检查微任务队列中,有Promise.then,执行微任务,发现有setTimeout是异步任务且宏函数,记做timer4放到宏函数队列;
10.微任务队列中的console.log(7)是同步任务,直接执行,打印7;
11.微任务执行完毕,第一次循环结束;
12.检查宏任务Event Table,里面有timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即Event Queue:timer2、timer4、timer3、timer1,取出排在第一个的timer2;
13.取出timer2执行,console.log(3)同步任务,直接执行,打印3;
14.没有微任务,第二次Event Loop结束;
15.取出timer4执行,console.log(6)同步任务,直接执行,打印6;
16.没有微任务,第三次Event Loop结束;
17.取出timer3执行,console.log(5)同步任务,直接执行,打印5;
18.没有微任务,第四次Event Loop结束;
19.取出timer1执行,console.log(2)同步任务,直接执行,打印2;
20.没有微任务,也没有宏任务,第五次Event Loop结束;
21.结果:1,4,8,7,3,6,5,2
第七题
setTimeout(function() { // timer1 console.log(1); setTimeout(function() { // timer3 console.log(2); }) }, 0); setTimeout(function() { // timer2 console.log(3); }, 0); 执行结果 //1,3,2
分析:
1.第一个setTimeout是异步任务且宏函数,记做timer1放到宏函数队列;
2.第三个setTimeout是异步任务且宏函数,记做timer2放到宏函数队列;
3.没有微任务,第一次Event Loop结束;
4.取出timer1,console.log(1)同步任务,直接执行,打印1;
5.timer1里面的setTimeout是异步任务且宏函数,记做timer3放到宏函数队列;
6.没有微任务,第二次Event Loop结束;
7.取出timer2,console.log(3)同步任务,直接执行,打印3;
8.没有微任务,第三次Event Loop结束;
9.取出timer3,console.log(2)同步任务,直接执行,打印2;
10.没有微任务,也没有宏任务,第四次Event Loop结束;
11.结果:1,3,2
页面性能
面试必考,这五个最好都能记住。异步加载和浏览器缓存都会延伸了问,其他三个只要说出来即可。
提升页面性能的方法有哪些?
- 资源压缩合并,减少 HTTP 请求
- 非核心代码异步加载(异步加载的方式,异步加载的区别)
- 利用浏览器缓存(缓存的分类,缓存原理)
- 使用 CDN
- 预解析 DNS
//强制打开 <a> 标签的 dns 解析
<meta http-equiv="x-dns-prefetch-controller" content="on">
//DNS预解析
<link rel="dns-prefetch" href="//host_name_to_prefetch.com">
复制代码
12.1 异步加载
异步加载的方式
- 动态脚本加载
- defer
- async
异步加载的区别
- defer 是在 HTML 解析完之后才会执行,如果是多个,按照加载的顺序依次执行。
defer
脚本会在DOMContentLoaded
和load
事件之前执行。 - async 是在脚本加载完之后立即执行,如果是多个,执行顺序和加载顺序无关。
async
会在load
事件之前执行,但并不能确保与DOMContentLoaded
的执行先后顺序。
下面两张图可以更清楚地阐述defer
和async
的执行以及和DOMContentLoaded
、load
事件的关系:
12.2 浏览器缓存
缓存策略的分类:
- 强缓存
- 协商缓存
缓存策略都是通过设置 HTTP Header 来实现的。
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识。
浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中。
12.2.1 强缓存
强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
1. Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
2. Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300
时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:
3. Expires和Cache-Control两者对比
其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
12.2.2 协商缓存
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效,返回304和Not Modified
- 协商缓存失效,返回200和请求结果
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
1. Last-Modified 和 If-Modified-Since
浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified 的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和 header;
Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
复制代码
浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200。
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag
和If-None-Match
2. ETag 和 If-None-Match
Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
3. 两者之间对比:
- 首先在精确度上,Etag要优于Last-Modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体 现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last- Modified也有可能不一致。
- 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
- 第三在优先级上,服务器校验优先考虑Etag
-
12.2.3 缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。
12.2.4 强缓存与协商缓存的区别
强缓存与协商缓存的区别可以用下表来表示:
缓存类型 | 获取资源形式 | 状态码 | 发送请求到服务器 |
---|---|---|---|
强缓存 | 从缓存取 | 200(from cache) | 否,直接从缓存取 |
协商缓存 | 从缓存取 | 304(Not Modified) | 是,通过服务器来告知缓存是否可用 |
用户行为对缓存的影响
用户操作 | Expires/Cache-Control | Last-Modied/Etag | |
---|---|---|---|
地址栏回车 | 有效 | 有效 | |
页面链接跳转 | 有效 | 有效 | |
新开窗口 | 有效 | 有效 | |
前进回退 | 有效 | 有效 | |
F5刷新 | 无效 | 有效 | |
Ctrl+F5强制刷新 | 无效 | 无效 |
12.2.5 from memory cache 与 from disk cache对比
在chrome浏览器中的控制台Network中size栏通常会有三种状态
1.from memory cache
2.from disk cache
3.资源本身的大小(如:1.5k)
三种的区别:
- from memory cache:字面理解是从内存中,其实也是字面的含义,这个资源是直接从内存中拿到的,不会请求服务器一般已经加载过该资源且缓存在了内存当中,当关闭该页面时,此资源就被内存释放掉了,再次重新打开相同页面时不会出现from memory cache的情况。
- from disk cache:同上类似,此资源是从磁盘当中取出的,也是在已经在之前的某个时间加载过该资源,不会请求服务器但是此资源不会随着该页面的关闭而释放掉,因为是存在硬盘当中的,下次打开仍会from disk cache
- 资源本身大小数值:当http状态为200是实实在在从浏览器获取的资源,当http状态为304时该数字是与服务端通信报文的大小,并不是该资源本身的大小,该资源是从本地获取的
状态 | 类型 | 说明 |
---|---|---|
200 | form memory cache | 不请求网络资源,资源在内存当中,一般脚本、字体、图片会存在内存当中。 |
200 | form disk ceche | 不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等。 |
200 | 资源大小数值 | 资源大小数值 从服务器下载最新资源。 |
304 | 报文大小 | 请求服务端发现资源没有更新,使用本地资源,即命中协商缓存。 |
十四、this,call,apply,bind
14.1 call 和 apply 共同点
- 都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。
- 调用 call 和 apply 的对象,必须是一个函数 Function
14.2 call 和 apply 的区别
区别主要体现在参数上。
call 的写法:
Function.call(obj,[param1[,param2[,…[,paramN]]]])
- 调用 call 的对象,必须是个函数 Function。
- call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
- 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
function func (a,b,c) {}
func.call(obj, 1,2,3)
// func 接收到的参数实际上是 1,2,3
func.call(obj, [1,2,3])
// func 接收到的参数实际上是 [1,2,3],undefined,undefined
apply 的写法
Function.apply(obj[,argArray])
- 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
- 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
function func (a,b,c) {}
func.apply(obj, [1,2,3])
// func 接收到的参数实际上是 1,2,3
func.apply(obj, {
0: 1,
1: 2,
2: 3,
length: 3
})
// func 接收到的参数实际上是 1,2,3
复制代码
14.3 call 和 apply 的用途
下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪个场景是必须用 call 或者必须用 apply 的,只是个人习惯这么用而已。
call 的使用场景
1、对象的继承。如下面这个例子:
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass();
// 1
复制代码
subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。
2、借用方法。还记得刚才的类数组么?如果它想使用 Array 原型链上的方法,可以这样:
slice
方法可以用来将一个类数组(Array-like)对象/集合转换成一个新数组。你只需将该方法绑定到这个对象上。
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
这样,domNodes 就可以应用 Array 下的所有方法了。
apply 的一些妙用
1、Math.max。用它来获取数组中最大的一项。
let max = Math.max.apply(null, array);
同理,要获取数组中最小的一项,可以这样:
let min = Math.min.apply(null, array);
2、实现两个数组合并。在 ES6 的扩展运算符出现之前,我们可以用 Array.prototype.push来实现。
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6]
14.4 bind 的使用
最后来说说 bind。在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
它的语法如下:
Function.bind(thisArg[, arg1[, arg2[, ...]]])
复制代码
bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 apply 和 call 则是立即调用。
来看下面这个例子:
function add (a, b) {
return a + b;
}
function sub (a, b) {
return a - b;
}
add.bind(sub, 5, 3); // 这时,并不会返回 8
add.bind(sub, 5, 3)(); // 调用后,返回 8
复制代码
如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。
总结
call 和 apply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。
bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
防抖节流, 详解;https://www.cnblogs.com/Antwan-Dmy/p/10714445.html
函数防抖和节流是优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。
防抖:触发高频函数事件后,n秒内函数只执行最后一次,如果在n秒内这个事件再次被触发的话,那么会重新计算时间。
节流:所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率
防抖和节流很象,一般用防抖多些
防抖小案例
场景;
- 有个输入框,输入之后会调用接口,获取联想词。但是,因为频繁调用接口不太好,所以我们在代码中使用防抖功能,只有在用户输入完毕的一段时间后,才会调用接口,出现联想词。
- 对于食品分类列表,我们频繁移动移出,会造成卡顿
注意点,我们需要将e(事件对象),this(dom对象)传递给submit(),
<input type="text"> <input type="submit" id="input"> <script> var btn =document.getElementById('input') btn.addEventListener('click',debounce(submit,1000),false) // btn.addEventListener('click',submit,false) // 进行防抖的事件处理,需要防抖的工作,这里执行 function submit(e){ console.log(e) //MouseEvent {isTrusted: true, screenX: 206, screenY: 128, clientX: 206, clientY: 25, …} console.log(this) //是事件对象input } // 封装的submit,防抖功能函数 function debounce(fn,time){ let timer // 当调用debounce函数,e事件对象会传入return的函数中接收,如果多个参数,可用arguments接收 return function(){ // 每次触发事件,清除请一个定时器 clearTimeout(timer)
timer = null // console.log(e); console.log(arguments[0]); //接收传入的第一个参数,e // timer= setTimeout(function(){ // fn(arguments[0]) //普通函数拿不到arguments[0],this指向的wendow // },1000) timer= setTimeout(()=>{ // fn.call(this,arguments[0]) //更改fn的this指向 fn.apply(this,arguments) //或者利用apply传参 },time) } } </script>
知识点补充:何为 arguments
?
我们举个例子:在 function test()
这个方法中,由于我们不确定变量有多少,比如 test("jsliang", 24)
,又或者 test("LiangJunrong", "jsliang", "24")
,这时候只需要在函数 test
中用 arguments
接收就行了。
在 function test() { let arr1 = argument[0] }
中,arr1
就可以获取到传进来的第一个变量。
fn.apply(this, arguments)
其实是将不确定变量替换到函数中了,fn函数只接受第一个参数
节流小案例,
场景
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
var btn =document.getElementById('input') btn.addEventListener('click',throttle(submit,2000),false) // btn.addEventListener('click',submit,false)
//需要节流的工作
function submit(e){ console.log(this,e) } function throttle(fn, time){ // 初始时间 let begin =0 return function(){ // 点击之后的时间 let cur = new Date().getTime() if(cur-begin >time){ fn.apply(this,arguments) begin =cur } } }
这样,在某些特定的工作场景,我们就可以使用防抖与节流来减少不必要的损耗。
那么问题来了,假设面试官听到你这句话,是不是会接着问一句:“为什么说上面的场景不节制会造成过多损耗呢?”
OK,这就涉及到浏览器渲染页面的机制了……
重绘与回流
在说浏览器渲染页面之前,我们需要先了解两个点,一个叫 浏览器解析 URL,另一个就是本章节将涉及的 重绘与回流:
- 重绘(repaint):当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制,因此损耗较少。
常见的重绘操作有:
- 改变元素颜色
- 改变元素背景色
- more ……
- 回流(reflow):又叫重排(layout)。当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。
常见的回流操作有:
- 页面初次渲染
- 浏览器窗口大小改变
- 元素尺寸/位置/内容发生改变
- 元素字体大小变化
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类(:hover……)
- more ……
- 重点:回流必定会触发重绘,重绘不一定会触发回流。重绘的开销较小,回流的代价较高。
看到这里,小伙伴们可能有点懵逼,你刚刚还跟我讲着 防抖与节流 ,怎么一下子跳到 重绘与回流 了?
OK,卖个关子,先看下面场景:
- 界面上有个 div 框,用户可以在 input 框中输入 div 框的一些信息,例如宽、高等,输入完毕立即改变属性。但是,因为改变之后还要随时存储到数据库中,所以需要调用接口。如果不加限制……
看到这里,小伙伴们可以将一些字眼结合起来了:为什么需要 节流,因为有些事情会造成浏览器的 回流,而 回流 会使浏览器开销增大,所以我们通过 节流 来防止这种增大浏览器开销的事情。
形象地用图来说明:
这样,我们就可以形象的将 防抖与节流 与 重绘与回流 结合起来记忆起来。
那么,在工作中我们要如何避免大量使用重绘与回流呢?:
- 避免频繁操作样式,可汇总后统一一次修改
- 尽量使用 class 进行样式修改,而不是直接操作样式
- 减少 DOM 的操作,可使用字符串一次性插入