JS闭包

作者: DeeJay_Y | 来源:发表于2017-08-18 21:03 被阅读0次

    一些基础概念

    变量的生命周期

    • 全局作用域下的变量,定义的时候开始存在,页面关闭或者刷新就消失。
    • 函数内部的变量,在你写的时候是不存在的,一般来说,只有调用这个函数的时候,这个变量才会被声明。那么这个函数内部的a什么时候消失呢,当a所在的作用域不见的时候a就消失。
    function fn () {
        var a = 1;
        // return undefined; 所有函数不写return时,默认有这一句
    }
    // 浏览器执行到这一行,此时a不存在
    
    fn();//这个时候a就诞生了
    //fn()执行完了之后,a就消失了
    fn();//这时候会产生一个新的a,并不是原来的a
    //fn() 执行完成后,新的a又消失了
    
    • 如果这个变量被引用着,就不能回收
    function fn () {
        var a = 1;
        var b = 2;
        window.xxx = a;
        // return undefined; 所有函数不写return时,默认有这一句
    }
    // 浏览器执行到这一行,此时a不存在
    
    fn();//这个时候a就诞生了
    //此时fn()执行完,b消失,a还存在,因为a被引用了。
    console.log(window.xxx); //1
    //等window消失a也就消失了
    //其实可以认为函数执行完成后,a这个变量名是消失了的,只是这段内存没消失,还在继续被引用,等window消失的时候再释放这段内存
    

    var作用域

    • 就近原则
    
        var a;
        function fn1 () {
            var a;
            function fn2 () {
                var a;
                a = 1; //给f2的a赋值
            }
        }
        
        function f2() {};
        function f1 () {
            function f2 () {}
            f2(); //调用的是f1内部的f2
        };
    
    • 词法作用域
        var a;
        function fn1 () {
            var a;
            function fn2 () {
                var a;
                a = 1; //给f2的a赋值
            }
        }
    

    不需要执行代码,分析语句的词法就能判断作用域叫词法作用域

    • 同名的不同变量

    立即执行函数

    • 如果想得到一个独立的作用域,必须要声明一个函数
    • 如果想运行函数内部的代码,必须要执行函数
      立即执行函数即创建一个匿名函数作用域并且立即执行。前面可以是! + - 等符号。
    • 关于有没有var声明的区别:
    
    var a = 100;
    
    !function() {
        var a = 1;
        !function() {
            a = 2; // 没有用var声明,所以不是当前作用域的a,而是上一级的作用域的a,所以改变了上一级a的值
            console.log(a);//2
        }();
        console.log(a);//2   a的值被改变 输出2
    }();
    console.log(a);//100
    
    • 立即执行函数的传参:
        var a = 1;
        !function(a) { //这里的a是个形参
            console.log(a); //外面传入一个a,里面的作用域输出2
        }(2);
        console.log(a); //全局作用域下的a还是1
    
    
        //上面的形参a其实相当于下面这么写
        !function(){
            var a = arguments[0];
            console.log(a);//外面传入一个a,里面的作用域输出2
        }(2);
    

    传参例子2

    
    var a = 100;
    
    !function(a){
        console.log(a);
    }(a);
    //这里的两个a,传入的a是外部传入的值,全局变量a,而匿名函数的a仅仅是个形参  等于不写参数,里面一个var a = arguments[0];
    //只是在执行函数的时候恰好把传入参数的a赋值给了内部的a
    

    变量(声明)提升

    • 浏览器在执行代码之前,会把所有声明提升到作用域的顶部
    var a = 100;
    var a = function () {};
    function a() {};
    console.log(a); //function
    //等价于
    var a;
    var a;
    function a () {};
    a = 100;
    a = function() {};
    console.log(a); // 所以是function
    
    • 函数内部的变量提升
    //函数内部的声明提升,不会提升到全局或者外部的作用域的,就只会提升到当前作用域顶部
    function f1() {
        // var a = 100; 变量提升等价于
        var a;
        a = 100;
    }
    f1();
    //另外,上述代码和下面的是一样的
    function f1() {
        a = 100;
        var a;
    }
    //只有在当前作用域中有var,就不会跑出当前作用域
    //如果是
    var a = 1;
    function f1 () {
        a = 100;
    //    后面语句里没有用var声明a,那么这个a就不是当前f1下的a。会在f1的上级作用域中寻找a,并且改变a的值
    }
    f1();
    console.log(a); //100
    
    • 带有迷惑性的变量提升
      先进行提升,在考虑代码
    var a = 100;  // 第一个a
    function f1 () {
        var b =2;
        if (b === 1) {
            var a; //第二个a
        }
        a = 99;
    }
    //上述例子中,f1内部写的a = 99指的是谁? 
    // 进行变量提升,首先明确JS没有块级作用域,所以什么if for都不是新的作用域,变量提升的时候也不用考虑if的判断条件
    //总而言之,先进行提升,在考虑代码,即f1内部等价于
    var a = 100;
    function f1 () {
        var b;
        var a;
        b = 2;
        if(b === 1){
            
        }
        a = 99;
    }
    //提升之后自然发现指的是f1内部的a
    

    闭包

    var items = document.querySelectorAll('li');
    for(var i = 0; i < items.length; i ++) {
        items[i].onclick = function () {
            console.log(i);
        }
    }
    

    上述例子分析:

    var items = document.querySelectorAll('li');
    for(var i = 0; i < items.length; i ++) {
        items[i].onclick = function () {
            console.log(i);
        }
    }
    console.log(i);
    
    //进行变量提升
    var items;
    var i;
    //这里提升了i,整个作用域中只有这一个i,所以循环到6之后,所有的i的值都为6
    items = document.querySelectorAll('li');
    for(i = 0; i < items.length; i ++) {
        items[i].onclick = function () {
            console.log(i); //C
        }
    }
    console.log(i); // D
    // 变量提升完成后 D是一定比C先执行的
    // 当D执行时 i已经循环为6,所以后面C输出的都是6
    
    
    //所以如果要想使得每次输出的值不一样,就得使得每次的i不是同一个i
    //要取得不同的i,就要创建新的作用域,那么我们来创建新的作用域:
    var items;
    var i;
    items = document.querySelectorAll('li');
    for(i = 0; i < items.length; i ++) {
        function temp (i) {//创建一个新的函数作用域,在这个作用域中传入每次循环得到的i的值,就能得到不一样的i了
        //    还要注意前面提到的问题,temp(i);中的i和函数的形参i不是同一个东西,具体见立即执行函数的传参
            items[i].onclick = function () {
                console.log(i); //接收到每次的i值然后赋给i,这样每次循环的i的值就不一样了
            }
        }
        temp(i); // 传入每次循环的i值
    
    }
    //那么前面提到的立即执行函数可以简化代码,不用写temp(),直接写个立即执行函数并且传入i就好了
    var items;
    var i;
    items = document.querySelectorAll('li');
    for(i = 0; i < items.length; i ++) {
        !function(i) {  //写成立即执行函数简化代码
            items[i].onclick = function () {
                console.log(i); //接收到每次的i值然后赋给i,这样每次循环的i的值就不一样了
            }
        }(i);
    
    }
    
    //上述代码就等价于下面的,temp函数时一个返回函数的函数,创造一个函数用于绑定事件
    var items;
    var i;
    items = document.querySelectorAll('li');
    for(i = 0; i < items.length; i ++) {
        function temp(i) {  // 这里temp(i)其实也是要提升的,但是只要知道内部的i跟外部i不一样就好了
            return function () {
                console.log(i);
            }
        }
    
        var fn = temp(i); //传入每次的i的值  fn必须为一个函数才能赋给onclick事件,所以temp的返回值必须也是一个函数
    
        items[i].onclick = fn;
    }
    
    
    // 有了返回值为一个函数的思想,我们可以直接给onclick赋给一个返回函数的函数,这样创建了一个新的作用域保存每次循环的i值,如下:
    var items;
    var i;
    items = document.querySelectorAll('li');
    for(i = 0; i < items.length; i ++) {
        items[i].onclick = function(i) {
            //函数内部作用域的i和外部的i不同
            return function (){
                console.log(i);
            }
        }(i); //传入每次循环的i值
    
    }
    
    

    总结一下思路:

    • 要得到每次循环的i值,就必须得创建新的作用域。因为全局作用域当中只有一个i,是不会变的,需要一个新的作用域来获取每次循环的i的值。
    • 我们给onclick绑定的一定是一个函数,所以赋给onclick一个立即执行函数之后,这个立即执行的值,即return的东西一定是一个函数。这就是为什么一定要return一个函数。

    通过作用域链理解闭包

    第一个例子:

        //通过作用域链理解闭包
    
    
        //下例中定义一个数组,遍历数组给每一项赋给一个函数,最后输出的 fnArr[1]的执行结果()   发现结果都是2,不管是fnArr[1]还是fnArr[0]
        // var fnArr = [];
        // for (var i = 0; i < 2; i ++) {
        //     fnArr[i] = function() {
        //         return i;
        //     }
        // }
        // console.log( fnArr[0]() ); //2
        // console.log( fnArr[1]() ); //2
        //分析: 当我们写一段函数体或者函数名的时候,就是一段代码,一个指针或者一个地址,只有后面加了括号(),它才会真正的去执行
        //没有执行的话,就相当于没有任何的作用,就是一段代码。
        //所以在上面的for循环中,赋给fnArr[i]的时候没有执行,当循环结束时,i的值为2,此时调用fnArr[1]()或者fnArr[0]并且输出,
        // 此时函数才会真正的执行,但是这个是函数里面没有i,只会向外部的全局作用域中寻找i,即for循环中的i,为2,所以不管fnArr[0]还是fnArr[1]都输出2
    
    
        //针对上面的例子,我们提出了对代码的改装要求,要求fnArr[0]就是输出0,fnArr[1]就是输出1。要做到这个效果,就要用到闭包
        // 第一种改装方法:
        // var fnArr = [];
        // for (var i = 0;i < 2; i ++) {
        //     (function(i){
        //         fnArr[i] = function () {
        //             return i;
        //         }
        //     })(i);
        // }
        // console.log( fnArr[0]() ); //0
        // console.log( fnArr[1]() ); //1
    //    针对第一种改装方法,就等价于下面这么写,
    //    第一种改装方法的改写1,去掉for循环:
    //     var fnArr = [];
    //     (function(i){
    //         fnArr[i] = function () {
    //             return i;
    //         }
    //     })(0);
    //     (function(i){
    //         fnArr[i] = function () {
    //             return i;
    //         }
    //     })(1);
    //     console.log( fnArr[0]() );
    //     console.log( fnArr[1]() );
    //    这种写法还有立即执行函数,不好理解,接着改写2,去掉立即执行函数,就等价于:
    //    第一种改装方法的改写2:
        var fnArr = [];
        function fn1 (i) {
            fnArr[i] = function fn11() {
                return i;
            }
        }
        function fn2 (i) {
            fnArr[i] = function fn22 () {
                return i;
            }
        }
        fn1(0);
        fn2(1);
    
        console.log( fnArr[0]() );
        console.log( fnArr[1]() );
    
    //    那么通过作用域链来分析改写2:
        globalContext = {
            AO : {
            //    活动对象,变量提升
                fnArr: undefined,
                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;
        //运行到fn1的时候,没有自身的AO里面没有fnArr,所以就要到scope里面去找,fn1.[[scope]]是globalContext的AO,找到了fnArr
        //此时globalContext的AO的fnArr的值就由undefined变为[fn11],然后什么都没发生,退出来进入fn2
        fn2Context = {
            AO : {
                i: 1,
                fn22: function,
            },
            scope: fn2.[[scope]],
        }
        fn22.[[scope]] = fn2Context.AO;
        //此时进入fn2,寻找fnArr和上面一样,过程结束后,globalContext的AO里面的fnArr的值为[fn11,fn22];
        //现在    console.log( fnArr[0]() ); 要输出fnArr[0]() ,即在globalContext中找到AO里面的fnArr[0]并且执行,此时进入fn11的context
        fn11Context = {
            AO: {
            //    AO是空的
            },
            scope: fn11.[[scope]],
        }
        // fn11没有AO,所以只能通过scope来寻找i,fn11.[[scope]]就是fn1Context.AO, 所以找到了i,i为0,
        // 一样的,当你输出fnArr[i]的时候,结果是调用fn22,然后找到fn2中的AO的i,为1
        fn22Context = {
            AO: {
                //    AO是空的
            },
            scope: fn22.[[scope]],
        }
        //那么对比我们一开始没有达成效果的例子,那个例子是因为在当前函数中找不到i,所以直接去全局中找到了i
        //我们现在改写了之后,在函数和全局之间又加了一层包装,在scope的中途就能获取到i,所以能够达成效果,这就是闭包
    

    第二个例子:

        //
        function fn () {
            var s  = 1;
            function sum () {
                ++s;
                console.log(s);
            }
            return sum;
        }
        var mySum = fn();
        mySum(); //2
        mySum(); //3
        mySum(); //4
        var mySum2 = fn();
        mySum2(); //2
        mySum2(); //3
        //上述写法等价于
        // function fn () {
        //     var s = 1;
        //     return function () {
        //         ++s;
        //         console.log(s);
        //     }
        // }
        //同样的,使用作用域链的伪代码来分析:
        globalContext = {
            AO : {
                fn: function,
                mySum: undefined,
                mySum2: undefined,
            }
        }
        fn.[[scope]] = globalContext.AO;
        //mySum = fn()时,进入fn的执行上下文
        fnContext = {
            AO: {
                s: 1,
                sum: function,
            }
            scope: fn.[[scope]]
        }
        sum.[[scope]] = fnContext.AO;
        //执行完成之后退出fn,此时globalContext的AO里面的mySum的值由undefined变为fn(),即sum,调用mySum(),即执行sum,此时进入sum的context
        sumContext = {
            AO : {
            //    为空
            },
            scope: sum.[[scope]],
        }
    //    sum没有AO,找不到s,所以到scope里面,即fn的AO里面找到s,此时s的值由1变为2.
    //    再次执行mySum(),此时sum还是没有s,继续到scope中去找,即找到了fnContext的AO的s,此时s为2,所以由2变为3
    
    //    然后执行到了mySum2 = fn(),此时,会初始化一个执行上下文,此时的s为1
        fnContext = {
            AO: {
                s: 1,
                sum: function,
            }
            scope: fn.[[scope]],
        }
        sum.[[scope]] = fnContext.AO;
    
    

    相关文章

      网友评论

        本文标题:JS闭包

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