闭包的理解&防抖节流
闭包是通过改变JS回收机制保留某作用域的一种手段。当一个函数执行完毕后,里面的局部变量是会被JS自带的垃圾回收机制给销毁的,从而释放内存。但是如果返回一个函数,而且函数里面有用到父级函数声明的变量,那么此时,变量不会被回收,因为还有可能被用到,并且外界可以通过函数访问这段作用域下的变量。
闭包: 函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。
可以在内部函数访问到外部函数作用域。使用闭包,一可以读取函数中的变量,二可以将函数中的变量存储在内存中,保护变量不被污染。而正因闭包会把函数中的变量值存储在内存中,会对内存有消耗,所以不能滥用闭包,否则会影响网页性能,造成内存泄漏。当不需要使用闭包时,要及时释放内存,可将内层函数对象的变量赋值为null。
函数执行分成两个阶段(预编译阶段和执行阶段)。
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
function foo(c){
var num = c;
return function A(){
num++;
return num;
}
}
var b = foo(5);//b = A
b();//6
b();//7
上面的代码中,调用了两次函数b,我们执行的是函数b(从foo返回出来的),并没有重新执行foo,所以也就不会每次给num重新赋值5。至于为什么会变成这种累加的情况呢,这是因为函数foo执行完后,其内部的的A函数里面对num有引用,所以foo的作用域以及变量a被保留在了函数A中,返回给了b。
闭包的案例
案例1
给4个li注册点击事件,输出点击的时候的li的索引,因为点击事件是在js执行完之后才产生的,因此如果没有设置自定义属性来记录li的话,此时打印出来的应该是每次都是 4
var heroes = document.getElementById('heroes');
var list = heroes.children;
for (var i = 0; i < list.length; i++) {
var li = list[i];
li.index = i;//这里设置了自定义属性
li.onclick = function () {
// 2 点击li的时候输出当前li对应的索引
console.log(this.index);
}
}
在这里我们可以用到闭包,把点击事件放到一个自执行函数中,传入自执行函数的参数为循环变量,这个时候就会发生闭包,由函数的执行环境可以知道,因为点击事件还咩有发生,所以传进来的参数i会保留存活在内存中,所以可以实现输出对应的索引值。
var heroes = document.getElementById('heroes'); var list = heroes.children; for (var i = 0; i < list.length; i++) { var li = list[i]; (function (j) { li.onclick = function () { // 2 点击li的时候输出当前li对应的索引 console.log(j); } })(i); }
案例2 定时器的执行过程
js代码在执行过程中会有执行栈和任务队列,当遇到setTimout的时候,setTimout是异步任务,会先setTimout里面的function放到任务队列里面,当执行栈上的代码执行之后,再去执行任务队列上的function;
console.log('start'); setTimeout(function () { console.log('timeout'); }, 0); console.log('over'); // 打印结果为:start over timeout
如果把定时器放在一个循环中,那么当遇到定时器的时候,会把定时器的函数放到任务队列之中,当循环结束之后,才会执行任务队列上的定时器函数,因此当i=2是,i++,此时i=3不再满足跳出循环,此时在任务队列上会保存有三个0,1,2定时器函数,当执行定时器函数的时候,i=3,此时会打印三个3,
for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 0);
// 3 3 3
因为:由于变量提升和异步任务,
可以想象成为这个样子
for (var i = 0; i < 5; i++) { } console.log('a'); setTimeout(function () { console.log('i = ' + i); }); setTimeout(function () { console.log('i = ' + i); }); setTimeout(function () { console.log('i = ' + i); }); setTimeout(function () { console.log('i = ' + i); }); setTimeout(function () { console.log('i = ' + i); });
那么对于上面的定时器函数在一个循环变量之中,如果我们想要每次记录下来循环变量的值,该怎么办呢?
方法1 、 使用let,可以记录每次循环变量的值,为什么?
因为,let存在块级作用域,for循环和定时器,共享同一个作用域。
由let声明的变量,每一次循环都会重新声明变量i,随后每一个循环都会使用上一个循环结束的值来初始化这个变量i
方法二、利用闭包
可以利用闭包,我们把定时器函数包括成一个自执行函数,并且他的参数为循环变量i这样每次循环的时候,就会在setTimeout函数之中保存一个i,当我们在执行任务队列上的定时器函数的时候,就会打印所有的变量i
console.log('start'); for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 0); })(i); } console.log('end');
// sart end 0 1 2
总结闭包的作用
作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数) 你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量。
作用3. 造成内存泄漏
闭包的举例
防抖/节流。
防抖
触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间;(不希望每次都触发,点击按钮请求接口)
思路:每次触发事件时都取消之前的延时调用方法:
function debounce(fn) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉 timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout // 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数 fn.apply(this, arguments); }, 500); }; } function sayHi() { console.log('防抖成功'); } var inp = document.getElementById('inp'); inp.addEventListener('input', debounce(sayHi)); // 防抖
高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。(滚动)
思路:每次触发事件时都判断当前是否有等待执行的延时函数。如果有等待执行的函数,则直接返回。
function throttle(fn) { let canRun = true; // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为 true,不为 true 则 return canRun = false; // 立即设置为 false setTimeout(() => { // 将外部传入的函数的执行放在 setTimeout 中 fn.apply(this, arguments); // 最后在 setTimeout 执行完毕后再把标记设置为 true(关键) 表示可以执行下一次循环了 // 当定时器没有执行的时候标记永远是 false,在开头被 return 掉 canRun = true; }, 500); }; } function sayHi(e) { console.log(e.target.innerWidth, e.target.innerHeight); } window.addEventListener('resize', throttle(sayHi));
节流的代码可以优化为下面
const throttle = (fn,timeout = 1000,_flag=true) => (...args)=>( _flag && setTimeout(()=>{ _flag = true; fn.apply(this,args)},timeout ) && (_flag = false)