js-拿下闭包 【干货】

46 阅读5分钟

要理解闭包,首先必须理解Javascript特殊的变量作用域

一、作用域

作用域类型:全局作用域函数作用域、ES6中新增了块级作用域,作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

函数作用域
是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了

块作用域

  1. 块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
  2. 在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问

1.1 var、let、const的区别

  1. var定义的变量,没有块的概念,可以跨块访问, 可以变量提升

  2. let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明

  3. const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明

// i是var声明的,在全局范围内都有效,全局只有一个变量i,输出的是最后一轮的i值,也就是 10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0]();  // 10

// 用let声明i,for循环体内部是一个单独的块级作用域,相互独立,不会相互覆盖
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function() {
    console.log(i);
  };
}
a[0](); // 0

1.2 作用域链

当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

    let x0 = 0;
    (function autorun1(){
     let x1 = 1;

     (function autorun2(){
       let x2 = 2;

       (function autorun3(){
         let x3 = 3;

         console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
        })();
      })();
    })();

二、闭包

闭包就是能够读取其他函数内部变量的函数

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

        function f1(){

            var n=999;

            function f2(){  
              alert(n);  
            }

            return f2;

      }

      var result=f1();

      result(); // 999

函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的,既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了,f2函数,就是闭包。

闭包的用途:

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

    function f1(){

        var n=999;

        nAdd=function(){n+=1}

        function f2(){  
          alert(n);  
        }

        return f2;

      }

      var result=f1();

      result(); // 999

      nAdd();

      result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

三、闭包的应用

    // 原始题目
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i); // 1s后打印出5个5
      }, 1000);
    }

    // ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4

    // 方法一:
    for (var i = 0; i < 5; i++) {
      (function(j) {
        setTimeout(function timer() {
          console.log(j);
        }, 1000);
      })(i);
    }

    // 方法二:
    // 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
    for (var i = 0; i < 5; i++) {
      setTimeout(function fn(i) {
        console.log(i);
      }, 1000, i); // 第三个参数i,将作为fn的参数
    }

    // ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
    for (var i = 0; i < 5; i++) {
      setTimeout(function fn(i) {
        console.log(i);
      }, 1000 * i, i);
    }

1. 防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

实现思路:

每次都执行闭包函数,只要有延时器id就清除掉 首次执不执行通过flag去控制

应用场景:  搜索框输入文字后调用对应搜索接口

     function debounce(fn, delay, flag = false) {
            let timer;
            return function (...args) {
                // 只要有timer就清除掉
                timer && clearTimeout(timer)
                // 是否首次执行函数调用
                if (flag && !timer) fn.apply(this, args)
                // 第一次指定没有timer  创建延时时间范围后执行函数调用 
                // this指向window 注意用的箭头函数 this由外部函数作用域决定
                timer = setTimeout(() => {
                    fn.apply(this, args)
                }, delay)
            }
        }


        // 代码测试
        function test(n) {
            console.log(`我执行了${n}次`);
        }
        let result = debounce(test, 2000, true)
        for (let i = 0; i < 5; i++) {
            result(i)
        }

2. 节流

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

实现思路:

每次都执行闭包函数,判断时间范围内有没有延时器id 没有去开启延时调用函数 同时将id=null 首次执不执行通过flag去控制 进入判断将开关 关闭

应用场景:  下拉滚动加载

    function throttle(fn, time, flag = false) {
            let timer;

            return function (...args) {
                // 首次是否执行
                if (flag) {
                    fn.apply(this, args)
                    flag = false
                }
                // 没有timer创建延时执行 每次执行完将timer = null 这样保证时间范围内只创建一个
                if (!timer) {
                    timer = setTimeout(() => {
                        fn.apply(this, args)
                        timer = null
                    }, time)
                }
            }
        }


        // 代码测试
        function test(n) {
            console.log('我执行了');
        }
        let result = throttle(test, 3000, true)
        setInterval(() => result(2), 500);

四、闭包的垃圾回收

副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收

好文推荐:

阮一峰老师

看一看

看一看