JS闭包及其使用场景

136 阅读2分钟

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

解决办法有三种,第一种是使用闭包的方式

for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}

第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

其他场景

  • 使用场景一:采用函数引用方式的setTimeout调用 setTimeout:接收两个参数,第一个参数可以是一段js代码,亦可以是一个函数,第二个参数是我们延迟执行第一个参数的时间(实际上不是延迟执行,而是延迟加入执行队列),在此我们要讨论的情况是第一个参数是一个函数的情况,我们传入的参数实际上是函数对象的引用,那这时候就不能向函数传参了,那么闭包就派上用场了
function fun(num){
  var age = num;
  return function(){
       console.log(age);
  }
}
​
var getAge = fun(200);//传入需要的参数,得到函数(闭包)的引用
var age = setTimeout(getAge,1000);//正确输出
  1. 防抖

触发高频事件后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)); // 防抖 

以这里为例,如果不使用闭包,代码可能是这样的:

function fangdou(){
    clearTimeout(timeout)
    var timeout = setTimeout(function(){
        console.log('防抖成功')
    },500)
}

这样是毫无效果的,基本上涉及到与上一次行为有关就要想到闭包的作用,因为闭包能够访问上次函数的私有变量

2.节流

高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率

  • 思路:

每次触发事件时都判断当前是否有等待执行的延时函数

function jieliu(fn) {
    //节流的原理就是类似锁,就是在一次请求没有执行完就不允许执行下一次请求
    //设置一个请求标志
    var flag = true
    if (flag == false) return;
    else {
        flag = false
        setTimeout(() => {
            //处理逻辑
            fn.apply(this, arguments)
            flag = true
        }, 5000)
    }
}
//上面的代码,如果不用闭包的话,每次执行jieliu函数,都会创建一个flag,这会导致每次执行jieliu都
//和上一次没有关系,这样做就毫无意义,我们应该要和上一次关联起来,基本上要和上一次关联起来就必须要使用闭包
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);
};
}