JS闭包,原理及应用场景

172 阅读3分钟

闭包是什么

闭包是个让人很迷惑的东西,特别是初学的时候,各种类型的函数都写不明白,接触到闭包确实有些难懂,这篇文章就彻底弄清楚闭包。

前置知识:作用域和作用域链

JS有三种作用域,全局作用域、函数作用域、块级作用域(ES6)新增。这里主要讨论全局作用域和局部作用域,因为块级作用域和闭包关系不大。

浏览器环境下,全局作用域就是window.代码执行过程中,会先创建一个全局的VO,里面存放了函数的地址,比如fn(){}:0Xb00。当函数被创建的时候,会再堆中开辟一块内存空间,就是函数变量,函数变量里存放的了他的父级作用域,函数变量和VO之间形成了引用。

      在内层作用域中,可以访问外层作用域的变量

所以在函数中可以访问到其父级作用域中的变量,这就形成了作用域链,定义了变量了一套访问规则。

闭包的产生场景

下面是一个最简单的闭包的例子,闭包也可以理解成一种现象。

            function createCounter() {
              let count = 0;
              return function() {
                count += 1;
                return count;
              }
            }
            ```
            let counter = createCounter();
            console.log(counter()); // 输出: 1
            console.log(counter()); // 输出: 2
            console.log(counter()); // 输出: 3
```

一句话说明白:内部的匿名函数可以访问到createCounter函数的私有(局部)变量count,而由于JS中函数是一等公民可以作为参数被返回,外面通过调用createCounter函数可以拿到内部函数,从而通过内部函数来实现对createCounter函数的私有(局部)变量的访问!就实现了在函数作用域外部访问函数内部变量。

几个经典问题的探讨

1.闭包造成内存泄漏

下面以outer作为外部函数,inner作为内部函数来描述。

闭包造成内存泄漏的原因是因为全局VO对象存在对inner函数的函数对象的引用(这里不懂的可以补一下VO,AO的知识),而inner函数的父级作用域与outer函数的AO对象形成了循环引用,导致两者都无法被释放,即outer函数的私有变量和inner函数看起来造成内存泄漏。

但是这就造成了内存泄漏吗?因为外面在接收到了outer函数,后续可能需要连续使用outer函数,这是外面的需求,但是一旦后续不再需要通过inner函数获取outer函数的私有变量,记得将接收内部函数的变量置为null,不然就会造成内存泄漏,所以说滥用闭包会造成内存泄漏!

2.闭包有“记忆性”并且接收到的函数相互独立

        function createCounter() {
                let count = 0;
                return function () {
                  count += 1;
                  return count;
                };
              }

      let counter1 = createCounter();
      let counter2 = createCounter();
      console.log(counter1()); // 输出: 1
      console.log(counter1()); // 输出: 2
      console.log(counter2()); // 输出: 1
      

上面的代码有两个意思:

   1.接收到inner函数的参数可以反复调用,count会改变,有记忆性。        
   2.如果用两个不同的参数接收inner函数,那么他们的调用彼此独立。   
   

闭包的使用场景

1.模拟私有变量与方法

        let counter = (function () {
                let privateCounter = 0;
                function changeBy(val) {
                  privateCounter += val;
                }
                return {
                  increment: function () {
                    changeBy(1);
                  },
                  decrement: function () {
                    changeBy(-1);
                  },
                  value: function () {
                    return privateCounter;
                  },
                };
              })();

        console.log(counter.value()); // 输出: 0
        counter.increment();
        counter.increment();
        console.log(counter.value()); // 输出: 2
        counter.decrement();
        console.log(counter.value()); // 输出: 1
        

在这个例子中,privateCounter变量和changeBy函数都是在立即执行函数表达式(IIFE)内部声明的,所以它们只能在该函数内部访问。但是,我们可以通过返回一个对象,该对象包含一些方法来访问和修改这些私有变量和方法。

柯里化(实现逻辑的复用)

            function curry(fn) {
                if (typeof fn !== "function") {
                  throw new Error("error");
                }
                function fn1(...args1) {
                  if (fn.length <= args1.length) {
                    return fn.apply(this, args1);
                  } else {
                    function fn2(...args2) {
                      return fn1.apply(this, args1.concat(args2));
                    }
                    return fn2;
                  }
                }
                return fn1;
              }

              function sum(a, b, c) {
                return a + b + c;
              }
          let curried = curry(sum);
          console.log(curried(1, 2)(3));
          console.log(curried(1)(2)(3));
          console.log(curried(1, 2, 3));`
          
          

上面是柯里化的实现方案,主要就是为了实现逻辑的复用,这个要会。