闭包的介绍——JS

引例

实现功能:3个按钮,点击按钮显示是第几个按钮。
实现:

<body>
    <button>button1</button>
    <button>button2</button>
    <button>button3</button>
    <script>
        var btns = document.getElementsByTagName('button')
        var length = btns.length
        for (var i = 0; i < length ; i++) {
           var btn = btns[i]
           btn.index = i
           btn.onclick = function (){
              alert("第"+(this.index+1)+'个按钮')
           }
        }
   </script>
</body>     

注意不能使用如下方式实现:

<body>
    <button>button1</button>
    <button>button2</button>
    <button>button3</button>
    <script>
        var btns = document.getElementsByTagName('button')
        var length = btns.length
        for (var i = 0; i < length ; i++) {
           var btn = btns[i]
           btn.onclick = function (){
              alert("第"+(i+1)+'个按钮')
           }
        }
    </script>
</body>

这样不管点击哪一个按钮都会显示是第4个按钮,这是因为,绑定事件和触发事件不是同时执行的。
代码运行的时候为按钮绑定事件,i变为3;但是当用户点击的时候才会触发事件,这时候i已经是3,所一不管点击哪一个按钮都输出4。

但是却可以这样写

<body>
    <button>button1</button>
    <button>button2</button>
    <button>button3</button>
    <script>
        var btns = document.getElementsByTagName('button')
        var length = btns.length
        for (var i = 0; i < length ; i++) {
           (function (i){
              var btn = btns[i]
              btn.onclick = function (){
                 alert("第"+(i+1)+"个按钮")
              }
           })(i)
        }
    </script>
</body>

运行结果正确,这就是利用了闭包。

闭包的定义

闭包初始

<body>
    <script>
        function fn1 (){
           var a =2
           function fn2 (){
              console.log(a)
           }
        //    注意:因为fn2函数没有被使用,被JS引擎优化掉了,所以在F12查看变量的时候就看不到了。
        // 在fn1函数的末尾加上'return fn2'可以解决此问题
           return fn2
        }
        fn1()
    </script>
</body>

有如下内容:在这里插入图片描述

闭包定义

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的 变量(函数) 时,就产生了闭包。
闭包的理解:
理解一:闭包是嵌套的内部函数(绝大部分人)
理解二:闭包是内部函数对象中,包含被引用变量(函数)的对象(极少数人)

以上面的例子为例。
在这里插入图片描述
注意:

  • 不管怎么说,闭包存在于嵌套的内部函数中
  • 闭包可以使用chrome调试查看

闭包产生的条件

  • 函数嵌套
  • 内部函数引用了外部函数的数据变量/函数
  • 执行外部函数
    (因为闭包在内部函数对象中,必须产生了内部函数对象才可能产生闭包,而执行外部函数才能执行内部函数定义,从而产生内部函数对象——调用外部函数,不用调用内部函数)
    —— 注意:这样说的前提是函数内部函数使用的是声明的方式定义函数,而不能使用变量的方式,因为声明的方式才能使函数声明提升。

常见的闭包

将函数作为另一个函数的返回值

        function fn1 (){
           var a =2
           function fn2(){
               a++
               console.log(a)
           }
           return fn2
        }
        var f=fn1()
        f()//fn2()
        f()

输出:

3
4

有两个问题:

  1. 产生了几个闭包?
  • 该过程中只创建了一个闭包
    闭包在产生内部函数对象时进行产生:上面的代码只是调用了一次fn1(),即函数执行上下文只产生一次,即对局部数据只进行一次预处理,函数内部函数对象自然也只产生一次,所以说只产生一个闭包。
    f()执行两次只是 fn2()执行了两次,和产生闭包没关系。
  • 所以说如果想要产生两个闭包,就需要执行两次fn1(),产生两个内部函数对象
    有结论:外部函数执行几次就产生几个闭包
  1. 为什么输出3、4?
  • 我们知道a是函数 fn1()的局部变量,执行 fn1()时产生,fn1()调用结束后销毁。所以执行完var f=fn1()语句后fn1()就调用结束,按理说应该销毁a。但是上述代码,a不但正常输出,调用两次fn2()还进行了累加,这说明 a没有被销毁的,或者说 a在在销毁前就被存在另一个地方,存在该地方后再使用a就可以直接在这里面拿。

  • 那这个地方是哪里呢,这个地方就是闭包。闭包在创建内部函数对象时产生,即外部函数执行时产生,并且当闭包所在的函数对象成为垃圾对象的时候才会消失。而 a 存放在闭包中自然也不会销毁。而闭包又在内部函数中,所以在内部函数中就可以对a进行操作。

  • 多个闭包之间不会冲突,多个闭包中存放的变量自然也不会冲突。eg:

        function fn1 (){
           var a =2
           function fn2(){
               a++
               console.log(a)
           }
           return fn2
        }
        var f1=fn1()
        f1()
        f1()
        var f2=fn1()
        f2()
        f1()
        f2()

输出:
在这里插入图片描述

补充:为什么上面说 执行完var f=fn1()语句后fn1()调用结束?

首先说一下调用堆栈的含义
调用堆栈是在调试程序时的一项内容,里面存放的是当前调用的函数,类似于执行上下文栈。
它的栈底存放的是全局的对象(匿名),栈顶存放的时当前调用的函数。
在这里插入图片描述
我在14、16、21行设置断点,进行逐句执行有如下结果:
(21行是代码执行的开始处)
执行顺序:21-14-19-22-16-17-18-23-16-17-18-24
在这里插入图片描述
执行的过程中观察右侧的调用堆栈,发现当执行到14-19 行的时候,调用堆栈有(匿名)和fn1,且栈顶是fn1()
在这里插入图片描述

执行 22 时,调用堆栈只有(匿名),即全局对象
执行16-17时调用堆栈有(匿名)和fn2,且栈顶是fn2()
在这里插入图片描述
即执行fn2()的时候,没有 fn1 ,说明此时 fn1已经调用执行完了。
即 执行完var f=fn1()语句后fn1()调用结束。

将函数作为实参传递给另一个函数调用

        // 将函数作为实参传递给另一个函数调用
        function showDelay (msg,time){
           setTimeout(function (){
            //    闭包中只有msg
              alert(msg)
           },time)
        }
        showDelay('baidu',2000)

输出:延迟两秒输出baidu弹窗
在这里插入图片描述
分析:闭包中只有msg

闭包的作用

  • 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
  • 让函数外部可以间接操作(读写)到函数内部的数据(变量/函数)

eg:

        function fn1(){
           var a =2
           function fn2(){
            a++
            console.log(a)
            
           }
           return fn2
        }
        var f = fn1()
        f()//3
        f()//4

输出:
在这里插入图片描述延长了局部变量的生命周期:指的是fn1()调用结束之后,通过 fn2() 任然可以访问fn1()的局部变量a
函数外部可以间接操作(读写)到函数内部的数据:指的是 f() 在函数fn1()的外面,但是仍然可以通过 f()a进行操作。

应用

通过上面闭包的两个作用,可以通过闭包实现如下功能:
如果我们不希望将一个变量完全暴露出去,想读就读想改就改,而是想要对该变量进行有限的操作,即只能对该变量执行我们规定的操作,就就可以使用闭包实现。
eg:上面的例子

      function fn1(){
         var a =2
         function fn2(){
          a++
          console.log(a) 
         }
         return fn2
      }
      var f = fn1()
      f()//3

该代码就实现了只能对变量a执行++操作,执行不了其他操作。
如果我们想让变量a只读,就可以修改成如下代码:

        function fn1(){
           var a =2
           function fn2(){
            return a;  
           }
           return fn2
        }
        var f = fn1()
        console.log('a='+f())

输出:
在这里插入图片描述

两个问题

  1. 函数执行完后,函数内部声明的局部变量是否还存在?
    一般是不存在,存在于闭包的变量才可能存在。
    为什么说是可能存在,是因为如果内部函数对象成为垃圾对象之后(即没有变量指向它),就会被回收,回收后内部函数中的闭包也会消失,闭包都不在了,变量自然也不能存在。
    eg:
      function fn1(){
         var a =2
         function fn2(){
          a++
          console.log(a) 
         }
         return fn2
      }
      fn1()

fn1()没有变量指向,即 fn2成为来及对象,即闭包所在的函数对象会被回收,纵然就不能保存局部变量了。

  1. 在函数外部能直接访问函数内部的局部变量吗?
    不能,但我们可以通过闭包让外部操作它。
    但是通过闭包操作函数内部变量的前提是:外部函数的返回值一般是内部函数,内部函数来对变量进行操作
    eg:上面的代码就是一个例子
      function fn1(){
         var a =2
         function fn2(){
          a++
          console.log(a) 
         }
         return fn2
      }
      var f = fn1()
      f()//3

分析: 分析如下代码,当代码执行到var f = fn1()时,谁会被释放,谁不会被释放

        function fn1(){
           var a =2
           function fn2(){
            a++
            console.log(a)
           }
           function fn3(){
            a--
            console.log(a)
           }
           return fn3
        }
        var f = fn1()
  • a不会被释放:有 f 指向 fn3对应的函数对象,fn3的闭包就不会消失,变量a就不会被释放。
  • 变量fn3被释放,但是它所对应的对象不会被释放: fn3对应的函数对象有变量 f 指向了自然不会被释放,但是 变量fn3 就没用了,会被释放掉。
  • 变量fn2被释放,它所对应的对象也会被释放 :外部没有变量指向fn2对应的对象,会成为垃圾对象被回收,变量fn2自然也没用了,也会被释放掉。

闭包的声明周期

  • 产生:在嵌套内部函数定义执行完时就产生了(不是在调用)
  • 死亡:在嵌套的内部函数成为垃圾对象时

因为闭包在内部函数对象中。

eg:

      function fn1(){
        //此时闭包就已经产生了(函数的提升,内部函数对象已经创建)
         var a =2
         function fn2(){
          a++
          console.log(a) 
         }
         return fn2
      }
      var f = fn1()
      
      //   闭包死亡:包含闭包的函数对象成为垃圾对象
      f=null

闭包应用

自定义JS模块

补充
JS模块的定义:具有特定动能js文件称为JS模块。
JS模块的规范:

  • 将所有的数据和功能都封装在一个函数内部 (函数内部的属性和方法私有的)
  • 只向外暴露一个或n个方法的函数或对象 (相当于将方法变成共有的)
    (暴露n个方法需要封装成对象)
    JS模块的调用:模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。

使用闭包实现JS模块
eg:
myModule.js文件:

function myModule() {
    // 私有的数据,默认定义的都是private数据
    var msg = 'baiDu'
    function doSomething (){
        console.log('doSomthing():' + msg.toUpperCase())
    }
    function doOtherthing (){
        console.log('doOtherthing():' + msg.toLowerCase())
    }

    // 封装成对象就可以返回多个函数,到时候想调谁就调谁
    // 向外暴露对象,return了就相当于将该函数或属性变成public了的
    return {
        // 变量名:函数名
        doSomething: doSomething,
        doOtherthing: doOtherthing
    }
}

引入:
.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <script type='text/javascript' src='./myModule.js'></script>
    
    <script>
    //先执行函数 
       var module = myModule()
       module.doSomething()
       module.doOtherthing()
    </script>
</body>
</html>

输出:
在这里插入图片描述
该JS模块使用了闭包
在这里插入图片描述
在这里插入图片描述
eg2:也可以使用匿名函数的自调用实现

myModule2.js文件:

(function (window){
    var msg = 'baiDu'
    function doSomething (){
        console.log('doSomthing():' + msg.toUpperCase())
    }
    function doOtherthing (){
        console.log('doOtherthing():' + msg.toLowerCase())
    }

    window.myModule2 = {
        doSomething: doSomething,
        doOtherthing: doOtherthing
    }
})(window)
// 虽然不传递window也可以在定义域链上从内向外找到window
// 但是传递window参数有利于后面的压缩

.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <script type='text/javascript' src='./myModule2.js'></script>
    
    <script>
    //    也使用了闭包,因为匿名函数的自调用已经调用了函数了,无需再手动调用
       myModule2.doSomething()
       myModule2.doOtherthing()
    </script>
</body>
</html>

也使用了闭包:匿名函数的自调用已经调用了函数了,无需再手动调用
在这里插入图片描述
我们一般使用匿名函数的自调用这种方式。

闭包的缺点

缺点

  • 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长,并且如果占用内存很大的话就会造成严重的空间浪费
  • 容易造成内存泄露

解决

  • 及时释放:及将闭包所在的函数对象设置为垃圾对象

eg:

        function fn1 (){
            // 闭包存在,占用很大空间,空间浪费
        var arr = new Array(100000)
           function fn2 (){
              console.log(arr.length)
           }
           return fn2
        }
        var f =fn1()
        f()
        // 释放
        f=null

习题

题1

       var name = "The Window";
       var object = {
           name:"My Object",
           getNameFunc : function (){
              return function(){
                  return this.name;
              }
           }
       }
       alert(object.getNameFunc()())
      // 输出:The Window 因为是吧函数返回之后才进行执行,
    //  (object.getNameFunc()返回值是function(){return this.name;},再直接执行该函数this就是window



       var name2 = "The Window";
       var object2 = {
           name2:"My Object",
           getNameFunc : function (){
            //    有闭包
              var that = this
              return function(){
                  return that.name2;
              }
           }
       }
       alert(object2.getNameFunc()())
    //    输出:My Object
    //  getNameFunc的this是 object2,that.name2即 object2.name

题2

      function fun (n,o){
         console.log(o)
         return{
             fun:function(m){
                return fun(m,n)
             }
         }  
      }

问:return fun(m,n)的fun是谁?
是外层函数function fun (n,o)
原因:
在这里插入图片描述

      function fun(n, o) {
        //   闭包,里面包含n
        console.log(o);
        return {
          fun: function (m) {
            return fun(m, n);
          },
        };
      }
      var a = fun(0); a.fun(1);a.fun(1);a.fun(1);//undefined 0 0 0
      var b = fun(0).fun(1).fun(2).fun(3);//undefined 0 1 2
      var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined 0 1 1

在这里插入图片描述
分析:var a = fun(0); a.fun(1);a.fun(1);a.fun(1);//undefined 0 0 0

  • 传参理解输出:
    a.fun(1)传递的参数是(1,0)所以输出 0
    a.fun(2)传递的参数是(2,0)所以输出 0
    a.fun(3)传递的参数是(3,0)所以输出 0
  • 闭包理解输出
    fun(0)输出undefined
    var a = fun(0),调用外层函数产生一个闭包,闭包里存放 n=0,而且有变量指向所以闭包不会消失。
    即 a 的闭包中存放的n一直等于0,又 a.fun(x) 后执行的语句是 console.log(n),所以不管x为几,console.log(n)只会取a的闭包中的n的值,即0。
    分析一下 a.fun(x) 的闭包情况
    接着使用 a.fun(1),又调用了外部函数,又会产生一个闭包,闭包里存放n=1,但是由于没有变量指向,所以闭包很快消失。

分析:var b = fun(0).fun(1).fun(2).fun(3);

  • 闭包理解输出
    fun(0)输出undefined
    var b = fun(0),调用外层函数产生一个闭包,闭包里存放 n=0,而且有变量指向所以闭包不会消失。
    紧接着 .fun(1),输出闭包中的n,即 0
    同时 .fun(1),又调用了外部函数,产生一个新闭包,闭包里存放 n=1,有变量指向所以闭包不会消失。
    紧接着 .fun(2),输出闭包中的n,即 1
    同时 .fun(2),又调用了外部函数,产生一个新闭包,闭包里存放 n=2,有变量指向所以闭包不会消失。
    紧接着 .fun(3),输出闭包中的n,即 2
    同时 .fun(3),又调用了外部函数,产生一个新闭包,闭包里存放 n=3,有变量指向所以闭包不会消失。

分析:var c = fun(0).fun(1); c.fun(2); c.fun(3);
这条语句其实就是上面两条语句的结合:

  • c = fun(0).fun(1):
    fun(0)输出undefined
    var c = fun(0),调用外层函数产生一个闭包,闭包里存放 n=0,而且有变量指向所以闭包不会消失。
    紧接着 .fun(1),输出闭包中的n,即 0
    同时 .fun(1),又调用了外部函数,产生一个新闭包,闭包里存放 n=1,有变量指向所以闭包不会消失。
  • c.fun(2);c.fun(3);
    即 c 的闭包中存放的n一直等于1,又 c.fun(x) 后执行的语句是 console.log(n),所以不管 x为几,console.log(n)只会取c 的闭包中 n 的值,几为1。

分析引例

我们再来分析一下开头得引例分析一下闭包结构

<body>
    <button>button1</button>
    <button>button2</button>
    <button>button3</button>
    <script>
        var btns = document.getElementsByTagName('button')
        var length = btns.length
        for (var i = 0; i < length ; i++) {
           (function (i){
              var btn = btns[i]
              btn.onclick = function (){
                 alert("第"+(i+1)+"个按钮")
              }
           })(i)
        }
    </script>
</body>

函数嵌套,内部函数引用外部函数变量,外部函数执行,所以产生了闭包:
在这里插入图片描述
数组长为3,执行了3次外部函数,产生了3个闭包,每个闭包中存放自己得 i 的值,所以输出的时候直接调用自己闭包中的 i 值,这样就是我们想要的输出结果了。

问题: 我们说如果包含闭包的对象函数被销毁,闭包就会死亡。
注意看这个自调用函数,没有变量指向外部函数,说明外部函数会成为垃圾对象,那内部函数对象是不是也销毁了呢?
注意 内部函数被 btn.onclick 指向,而 btns是一个全局变量不会被销毁(页面不关闭,按钮不释放),所以内部函数对象不hi被销毁,闭包也不会死亡。
如果想要释放设置:btn.οnclick=null

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值