美文网首页
初识闭包

初识闭包

作者: 学的会的前端 | 来源:发表于2019-02-25 10:25 被阅读0次

    闭包

    闭包就是指有权访问另一个函数作用域中的变量的函数。
    在后台执行环境中,闭包的作用域链包含着它自己的作用域,包含函数的作用域和全局作用域。
    作用:暴露局部变量。
    JS 中的闭包是什么?

    • 闭包的表现形式
    //形式一
        function(){
            local++
            console.log(local)
        }
        //形式二
        function foo(){
            var local = 1
            function bar(){
                local++
                returnlocal
            }
            return bar
        }
        var fnc = foo()
        fnc()
    

    示例1引入闭包

        var fnArr = [];
        for(var i = 0; i < 2; i++){
            fnArr[i] = function(){
                return i;
            }
            //以上代码的函数其实并没有执行,一直在赋值而已。for循环遍历之后
            //才调用函数,此时,i已经等于2,所以调用函数输出的是全局变量2.
            //数组的每一项是一个函数
            //fnArr[1]是一个函数
        }
        console.log(fnArr[1]()); //输出结果为2
        console.log(fnArr[0]()); //输出结果为2
        console.log(fnArr[2]()); //报错,fnArr[2] is not a function
    
    捕获.PNG
    以上代码进行改装,使其输出的就是对应的i值。
        //方法1:
        var fnArr = [];
        for(var i = 0; i < 2; i++){
            (function(i){
                fnArr[i] = function(){
                    return i;
                }
            })(i) // 立即执行函数
        }
    // 代码改写
        var fnArr = [];
        function fn1(i){
            fnArr[i] = function fn11(){
                return i
            }
        }
        function fn2(){
            fnArr[i] = function fn22(){
                return i
            }
        }
        fn1(0)
        fn2(1)
    

    利用作用域链理解一下代码:

    • 当执行一个函数,就会初始化一个活动对象。
    • 当在执行上下文A创建函数fn,fn的作用域链就指向A的活动对象。
        globalContext = {
            AO: {
                fnArr: [fn11,fn22];
                fn1: function;
                fn2: function;
            }
        }
        fn1.[[scope]] = globalContext.AO
        fn2.[[scope]] = globalContext.AO
        fn1Context = {
            AO: {
                i: 0
                fn11: function;
            }
            scope: fn1.[[scope]]
        }
        fn11.[[scope]] = fn1Context.AO
        fn2Context = {
            AO: {
                i: 1
                fn22: function;
            }
            scope: fn2.[[scope]]
        }
        fn22.[[scope]] = fn1Context.AO
        fn11Context = {
            AO: {
    
            }
            scope: fn11.[[scope]]
        }
        fn22Context = {
            AO: {
    
            }
            scope: fn22.[[scope]]
        }
    

    示例2引入闭包

        function fn(){
            var s = 1
            function sum(){
                ++s
                console.log(s)
            }
            return sum
        }
    // 以上代码可以改写成
        function(){
            var s = 1
            return function(){
                ++s
                console.log(s)
            }
        }
        var mySum = fn()
        mySum() // 2
        mySum() //重新调用的话,就会初始化一个新的sumContext 3
        mySum() // 4
        var mySum2 = fn()
        mySum2() // 2
        mySum2() // 3
    

    利用作用域链解释:

    • 再一次重新调用函数所产生的执行上下文与之前的没有任何关系。
    globalContext = {
            AO: {
                fn: function
                mySum: sum
                mySum2: undefined
            }
        }
        fn.[[scope]] = globalContext.AO
        fnContext = {
            AO: {
                s: 2
                sum: function
            }
            scope: fn.[[scope]]
        }
        sum[[scope]] = fnContext.AO
        sumContext = {
            AO: {}
            scope:sum.[[scope]]
        }
        //调用函数会初始化一个新的执行上下文,与前一个没有任何关系
        fn-Context = {
            AO: {
                s: 1
                sum: function
            }
            scope: fn.[[scope]]
        }
        sum.[[scope]] = fn-Context.AO
    

    变量(内存)的生命周期

    1. 默认作用域消失时,内存就被回收。
    2. 全局变量的生命周期:
        var a = 1;
        // 浏览器一行行执行代码,当执行到这一行a的值 1 就出现在内存中了
        // window窗口关闭(关闭页面),a就消失
        // 刷新页面的时候,之前的a也消失不见了,执行代码会出现新的a
        生命周期不会超过页面的生命周期
    
    1. 局部变量的生命周期:
    function f1(){
            var a = 1;
            return undefined;//所有的函数不写return,默认return undefined。
        }
        // 函数f1被调用的时候,a才存在(出生)。此时,a不存在
        f1();
        //浏览器调用f1,当执行到a所在的一行,a才会出现在内存中。a的取值是1.
        //当a所在的环境作用域不在的时候,a就不存在了。当函数执行return之后,跳出函数的执行环境,a就不存在了。
        f1();
        //再一次调用f1,则产生了新的a,与原来的a没有任何关系
    
    1. 如果变量被引用着,则不能回收。
        function f1(){
            var a = {name: 'a'};
            var b = 2;
            window.xxx = a; //这不是覆盖
        }
        f1();
        // 函数执行完b就死了
        // 函数执行完,a还存在。
        console.log(a)
        //此时,a可以被引用,a的值为1,window死了,a才会死。
        //也可以认为,变量名死了,但真正的内存还存在
        window.xxx = {name:'b'}
        //此时a已经没有被引用了,就消失了
    

    var作用域

    • 就近原则
      在当前的作用域找是否有同名的,没有则在父级作用域找
            var a
            function f1(){
                var a  //a的值为1,只看父级作用域,而且就近不是指的代码近。
                function f2(){
                    
                    a = 1
                }
                function f3(){
                    var a
                }
            }
    

    函数的作用域的就近原则:

        function f2(){}
        function f1(){
            functon f2(){
    
            }
            f2() //指的是f1当中的f2
        }
    
    • 词法作用域
      只要看层级关系,分析语句的词法,就可以确定a到底是谁,不需要执行代码
    var a //2
        function f1(){
            var a
            function f2(){
                var a
                f3()
                a = 1
            }
        }
        function f3(){
            a = 2 //指的是第一个a,和f3执行与否没有任何关系
        }
    
    
    • 同名的不同变量
        function f1(){
            var a
            function f2(){
                var a
                a = 1
            }
        }
        //f2中的a与f1中的a只是名字相同,但是没有任何的关系
    

    立即执行函数

    • 如果想得到一个独立的作用域,必须声明一个函数
      目的:不产生全局变量
    function f1(){
            var a
            a = 1
            console.log(a)
        }
    
    • 如果想运行函数内的代码,必须执行函数
    function f1(){
            var a
            a = 1
            console.log(a)
        }
    f1()//f1()此时还是一个全局变量
    
    • 为了避免产生全局变量
        function(){
            var a
            a = 1
            console.log(a)
    
        }()
    //声明了一个匿名函数,没有全局变量。
    //此时的函数浏览器运行时会报错,语法错误。
    
    • 函数写法改进(立即执行函数),避免语法报错
    !function(){
      var a
      a = 1
      console.log(a)
    }()
    
    • 立即执行函数传参
    第一种情况
    var a = 2
        !function(a){
            
            a = 1 //形参声明的a
            //形参是给第一个参数赋值
            //这里面的a是一个新的作用域,与外面的a没有任何关系
            console.log(a) //1
    
        }(/*没有传参*/)
        console.log(a) //2
    第二种情况
            var a = 100
        !function(a){ //此处的a与调用传入的参数a没有任何关系,只是名字相同
            console.log(a) //这个a是上面离他最近的a
        }(a) //此处的a为全局作用域下的a,取值为100  
    

    变量提升

    • 浏览器在执行代码之前,会把所有的声明提升到作用域的顶部。
        function a(){}
        var a = 100
        console.log(a) // 100
        //变量提升
        var a 
        function(){}
        a = 100
        console.log(a) //100
    
        var a = 100
        function a(){}
        console.log(a) // 100
        //变量提升
        var a
        function(){}
        a = 100
        console.log(a) //100
    
            var a = 100
            var a = function(){}
            function a(){}
            console.log(a)
            //变量提升
            var a 
            var a
            function(){}
            a = 100
            a = function(){}
            console.log(a) // function(){}
    
            function f1(){
          a = 1
          var a
        }
        f1()
        //相当于
            function f1(){
            var a
            a = 1
        }
        f1()
    
    
        var a = 100
        f1()
        function f1(){
            var b = 2
            if(b === 1){
                var a //a=99指的是此处的a
            }
            a = 99
        }
        //声明提升
        var a
        function f1(){
            var b
            var a
            b = 2
            if(b === 1){
    
            }
            a = 99
        }
        a = 100
        f1()
    
    • 提升变量很重要!(一定要动手提升变量。)

    时机(异步)

    • 先写的代码后执行,后写的代码先执行。
        button.onclick = function f(){
            console.log(1)//发生点击事件的时候才会执行代码
        }
        console.log(2)//代码一定会执行
        //先console.log(2),之后在console.log(1).
        //当用户点击按钮时,浏览器会执行函数f()
        //button.onclick(event),浏览器手动的执行这句话
        //button.onclick.call(target,event)
    

    内存泄露

    内存泄露的例子

    var fn = function(){
            var a = {
                name: 'a'
            }
            var b = {
                name: 'b'
            }
            return function(){
                return a
            }
        }()
        console.log(fn())
        //b如果没有被回收,就是内存泄露,谁也不能访问b了,IE会出现内存泄露
    
    

    如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。

        function assign(){
            var element = document.getElementById('some')
            element.onclick = function(){
                alert(element.id)
            }
        }
    

    由于匿名函数保存了一个对于assign()的活动对象的引用,因此就会导致无法减少element对的引用数,只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。

        function assign(){
            var element = document.getElementById('some')
            var id = element.id
            element.onclick = function(){
                alert(id)
            }
              element = null
        }
    

    在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用,但是此时还不能解决内存泄露的问题。闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中仍会保存一个引用。因此,有必要把element变量设置成null,这样就可以消除对DOM对象的引用,顺利的减少其引用数,确保正常回收其占用的内存。

    面试题

    闭包是造成问题的原因,立即执行函数是解决问题的方法。

        var items = document.querySelectorAll('li')
        for(var i = 0; i < items.length; i++){
            items[i].onclick = function(){
                console.log(i)
            }
        }
        //运行结果:无论i为多少,console.log(i)的值都为6.
        //变量提升
        var items
        var i
        for(i = 0; i < items.length; i++){
            //i == 0,1,2,3,4,5
            items[i].onclick = function(){
                console.log(i) //C
            }
        } 
        // i == 6
        console.log(i) //D 6
        //D一定会执行,C在D后面执行
    
    i打印出值都为6.PNG

    解决办法:

    方法一:
        var items
        var i
        for(i = 0; i < items.length; i++){
            !function(i){
                items[i].onclick = function(){
                console.log(i) 
                }
            }(i)        
        } 
    方法二:
    var items
        var i
        items = document.querySelectorAll('li')
        for(i = 0; i < items.length; i++){
            items[i].onclick = function(i){
                return function(){
                    console.log(i)
                }
            }(i)
                  
            //自执行函数是有返回值的
    //循环的时候把i的值赋值给了新的i,新的i是不会i++.
    //console.log(i)的执行是很后面的,当执行的时候,i已经为6了。
        }
    

    相关文章

      网友评论

          本文标题:初识闭包

          本文链接:https://www.haomeiwen.com/subject/mqhwyqtx.html