4 闭包

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数,

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。

  • (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

4.1 闭包产生的原因

首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————全局作用域函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:

    var a = 1;
    function f1() {
      var a = 2
      function f2() {
        var a = 3;
        console.log(a);//3
      }
    }

在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:

    function f1() {
      var a = 2
      function f2() {
        console.log(a);//2
      }
      return f2;
    }
    var x = f1();
    x();

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

  • 那是不是只有返回函数才算是产生了闭包呢?
  • 回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:
    var f3;
    function f1() {
      var a = 2
      f3 = function() {
        console.log(a);
      }
    }
    f1();
    f3();
  • f1执行,给f3赋值后,等于说现在f3拥有了window、f1和f3本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a,因此输出2。
  • 在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变

4.2 闭包有哪些表现形式

明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?

  1. 返回一个函数。刚刚已经举例。
  2. 作为函数参数传递
    var a = 1;
    function foo(){
      var a = 2;
      function baz(){
        console.log(a);
      }
      bar(baz);
    }
    function bar(fn){
      // 这就是闭包
      fn();
    }
    // 输出2,而不是1
    foo();
  1. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包

以下的闭包保存的仅仅是window和当前作用域。

    // 定时器
    setTimeout(function timeHandler(){
      console.log('111');
    }100)
    
    // 事件监听
    $('#app').click(function(){
      console.log('DOM Listener');
    })
  1. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以访问全局的变量
    var a = 2;
    (function IIFE(){
      // 输出2
      console.log(a);
    })();

4.3 如何解决下面的循环输出问题

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

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好) 因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

  1. 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
    for(var i = 1;i <= 5;i++){
      (function(j){
        setTimeout(function timer(){
          console.log(j)
        }, 0)
      })(i)
    }
  1. 给定时器传入第三个参数, 作为timer函数的第一个函数参数
    for(var i=1;i<=5;i++){
      setTimeout(function timer(j){
        console.log(j)
      }, 0, i)
    }
  1. 使用ES6中的let
    for(let i = 1; i <= 5; i++){
      setTimeout(function timer(){
        console.log(i)
      },0)
    }

let使JS发生革命性的变化,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。代码的作用域以块级为单位,以上面代码为例:

    // i = 1
    {
      setTimeout(function timer(){
        console.log(1)
      },0)
    }
    // i = 2
    {
      setTimeout(function timer(){
        console.log(2)
      },0)
    }
    // i = 3
    ...

4.4 闭包的几种使用场景

1. 返回值(最常用)

    //1.返回值 最常用的
    function fn(){
        var name="hello";
        return function(){
            return name;
        }
    }
    var fnc = fn();
    console.log(fnc())//hello

这个很好理解就是以闭包的形式将 name 返回

2. 函数赋值

    var fn2;
    function fn(){
        var name="hello";
        //将函数赋值给fn2
        fn2 = function(){
            return name;
        }
    }
    fn()//要先执行进行赋值,
    console.log(fn2())//执行输出fn2

在闭包里面给fn2函数设置值,闭包的形式把name属性记忆下来,执行会输出 hello。

3. 函数参数

    function fn(){
        var name="hello";
        return function callback(){
            return name;
        }
    }
    var fn1 = fn()//执行函数将返回值(callback函数)赋值给fn1,
     
    function fn2(f){
        //将函数作为参数传入
        console.log(f());//执行函数,并输出
    }
    fn2(fn1)//执行输出fn2

用闭包返回一个函数,把此函数作为另一个函数的参数,在另一个函数里面执行这个函数,最终输出 hello

4. IIFE(自执行函数)

    (function(){
        var name="hello";
        var fn1= function(){
            return name;
        }
        //直接在自执行函数里面调用fn2,将fn1作为参数传入
        fn2(fn1);
    })()
    function fn2(f){
        //将函数作为参数传入
        console.log(f());//执行函数,并输出
    }

直接在自执行函数里面将封装的函数fn1传给fn2,作为参数调用同样可以获得结果 hello

5. 循环赋值

    //每秒执行1次,分别输出1-10
    for(var i=1;i<=10;i++){
        (function(j){
            //j来接收
            setTimeout(function(){
                console.log(j);
            },j*1000);
        })(i)//i作为实参传入
    }

如果不采用闭包的话,会有不一样的情况

6. getter和setter

    function fn(){
        var name='hello'
        setName=function(n){
            name = n;
        }
        getName=function(){
            return name;
        }
    
        //将setName,getName作为对象的属性返回
        return {
            setName:setName,
            getName:getName
        }
    }
    var fn1 = fn();//返回对象,属性setName和getName是两个函数
    console.log(fn1.getName());//getter
    fn1.setName('world');//setter修改闭包里面的name
    console.log(fn1.getName());//getter

第一次输出 hello 用setter以后再输出 world ,这样做可以封装成公共方法,防止不想暴露的属性和函数暴露在外部

7. 迭代器(执行一次函数往下取一个值)

    var arr =['aa','bb','cc'];
    function incre(arr){
        var i=0;
        return function(){
            //这个函数每次被执行都返回数组arr中 i下标对应的元素
             return arr[i++] || '数组值已经遍历完';
        }
    }
    var next = incre(arr);
    console.log(next());//aa
    console.log(next());//bb
    console.log(next());//cc
    console.log(next());//数组值已经遍历完

8. 缓存

    // 比如求和操作,如果没有缓存,每次调用都要重复计算,采用缓存已经执行过的去查找,查找到了就直接返回,不需要重新计算    
    var fn=(function(){
      var cache={};//缓存对象
      var calc=function(arr){//计算函数
        var sum=0;
        //求和
        for(var i=0;i<arr.length;i++){
          sum+=arr[i];
        }
        return sum;
      }
    
      return function(){
        var args = Array.prototype.slice.call(arguments,0);//arguments转换成数组
        var key=args.join(",");//将args用逗号连接成字符串
        var result , tSum = cache[key];
        if(tSum){//如果缓存有   
          console.log('从缓存中取:',cache)//打印方便查看
          result = tSum;
        }else{
          //重新计算,并存入缓存同时赋值给result
          result = cache[key]=calc(args);
          console.log('存入缓存:',cache)//打印方便查看
        }
        return result;
      }
    })();
    fn(1,2,3,4,5);
    fn(1,2,3,4,5);
    fn(1,2,3,4,5,6);
    fn(1,2,3,4,5,8);
    fn(1,2,3,4,5,6);

Last Updated:
Contributors: leeguooooo