闭包

前言

JavaScript的闭包与作用域链密不可分,因此本文可以和JavaScript的作用域链相对照分析,一定可以对JavaScript的闭包和作用域链有更深的理解。

什么是闭包?

由父函数与子函数组成,子函数在调用的时候读取父函数的变量,就产生了闭包
看一个栗子

function box(){
    var arr = [];
    for(var i=0;i<5;i++){
        arr[i] = i;        
    }
    return arr;
}
console.log(box());//0 1 2 3 4    

上面这个例子是我们平时接触得最多的简单的for循环,她可以按预期输出数组[0, 1, 2, 3, 4],但是有时候我们需要在for循环里面添加一个匿名函数来实现更多功能,看下面的代码

    function box() {
            var arr = [];
            for (var i = 0; i < 5; i++) {
                arr[i] = function () {
                    return i; 
                } 
            } 
            return arr;
        }
        console.log(box()[0]());//5
        console.log(box()[1]())//5

上面这段代码无法输出我们预期的效果,而是每次执行都会返回5,这是为什么呢?

  • 上面这个栗子就是闭包引起的问题,那么闭包产生原理是什么?我们要怎么解决闭包问题?让我们一起探索闭包的奥秘吧~

产生闭包的条件

1.有父子函数的关系
2.子函数使用了父函数的变量
3.子函数有调用

    function wrapFn(){
            var a=10;
            function innerFn1(){
                return a;   //innerFn1是一个子函数,它的里用到了a,这个a是父级的。但是这个函数并没有调用 
            }
            function innerFn2(){    //innerFn2是一个子函数,它里面没有用到a。但是它调用了。所以就让父函数形成了一个闭包环境
                //debugger;
            }

            innerFn2();

            //debugger;
        }
        wrapFn();
  • 先举几个简单的栗子
     function father(){
            var a=10;
            function son(){
                a++;
                console.log(a); //11
            }
            son();
        }
        father();

     function father(){
            var a=10;
            function son(){
                a++;
                console.log(a); //11
            }
            son();
        }
        father();

      function father(){
            var a=10;
            function son(){
                a++;
                console.log(a); //11
            }
            son();
        }
        father();

上面就是闭包的表面现象了

闭包底层原理

边介绍概念边理解闭包

1.变量的生命周期
1.1局部变量的生命周期在函数执行完成以后就到头了
1.2全局变量的生命周期在页面关闭后就到头了

    var a=1;//全局变量
         function fn(){
            var a=10;//局部变量
        }
        fn();
        console.log(a); 

当没有全局变量a定义时,直接console.log(a) ;这是会报a is not defined的错误,这是因为在fn()函数执行完毕后局部变量a就被销毁了

2.垃圾回收机制
2.1标记清除
2.2引用计数(常用)
注意:如果说这个数据有引用的关系,就不会被回收

3.普通函数与闭包函数
3.1普通函数,定义函数的时候是嵌套的,调用的时候也是嵌套的
3.2闭包函数,定义函数的时候是嵌套的,调用的时候是独立的、

4.执行上下文(当前代码的执行环境,EC)
4.1全局环境
4.2函数环境
4.3eval环境(听说不常用)

执行上下文可以理解为代码的执行环境

5.执行上下文栈(ECS),函数调用栈(call stack)
介绍一下执行上下文栈和函数调用栈的使用过程

      function father(){
            debugger;   //打上一个断点,用来调试
            function son(){
                debugger;
            }

            son();
            debugger;
        }

        father();
        debugger; 

过程

执行father()

执行son()

son()执行完毕

father()执行完毕

可以看出函数调用栈类似于数据结构中的栈,遵循先进后出的原则,所以全局的上下文先入栈,再到father的上下文入栈,最后是son的上下文,出栈则相反。

6.创建完EC后需要走两个阶段
6.1创建阶段
6.2代码执行阶段

          function father(){
            
            var n=30;
            debugger;
            function son(){
                n++;
                debugger;
                console.log(n);
            }

            return son;
        }

        var result=father();
        result();   //31
        result();   //32

上述代码中有父函数(father)和子函数(son),父函数返回了子函数的引用,并赋值给了result变量,father()已经执行完毕了,如果没有返回son()函数的引用,根据垃圾回收机制的原理,father()函数所占用的空间会被回收,但是现在返回了引用,就不会被回收,这时在全局环境下执行reslut()(实际上是执行son()函数),就形成了闭包,当son()的作用域里没有某一个变量(这里是n)时,JS引擎就会通过作用域链一层层往上找,直到找到为止,所以闭包也可以定义为在某个函数的作用域之外执行

下面看一下执行上下文的创建过程

fatherEC的内部走的路子

//1、创建阶段
         fatherEC={
            VO:{    //变量对象
                n:undefined //找变量声明
                //son:'son在内存里的引用地址'
            },
            scope:[ //存储作用域链
                Global.AO
            ],
            //this:'window'
        }

        //2、执行阶段
        fatherEC={
            AO:{    //变量对象
                n:30    //变量赋值
            },
            scope:[ //存储作用域链
                fatherEC.AO,Global.AO
            ],
            //this:'window'
        }

sonEC的内部走的路子

        
        //1、创建阶段
        sonEC={
            VO:{    //变量对象
            },
            scope:[ //存储作用域链
                fatherEC.AO,Global.AO
            ],
            //this:'window'
        }

        //2、执行阶段
        sonEC={
            AO:{    //变量对象
            },
            scope:[ //存储作用域链
                sonEC.AO,fatherEC.AO,Global.AO
            ],
            //this:'window'
        } */

举多一个容易混淆的栗子

        var a=20;
        function wrapFn(){
            debugger;   //这个函数就是一个闭包函数
            var b=10;
            debugger;
            function inerFn(){
                debugger;
                //var b=b;
                console.log(b);//10
                debugger;
            }

            return inerFn;
        }
        var fn=wrapFn();
        fn(a); 

相信大家都猜出来这时打印b的结果是10,如果把var b = b;的注释删除,你猜到是什么了吗,没错!!!是undefined,如果有不明白的可以在看看上一个栗子创建EC需要走的两个阶段的栗子,马上就能明白其实var b = b;实质是var b = undefined;

过程图也给你们发一下

  • wrapFn()函数创建


    wrapFn()创建
  • wrapFn()函数执行


    wrapFn()执行
  • innerFn()函数创建


    inerFn()创建
  • innerFn()函数执行


    inerFn()执行

解决闭包问题

相信通过上面的讲解你已经对闭包有一定的了解了呢!现在回归文章一开始的问题,解释一波~看代码

function box() {
            var arr = [];
            for (var i = 0; i < 5; i++) {
                debugger;
                arr[i] = function () {
                    return i; //由于这个闭包的关系,他是循环完毕之后才返回,最终结果是4++是5
                }
                //function(){}这个匿名函数里面根本没有i这个变量,所以匿名函数会从父级函数中去找i,当找到这个i的时候,for循环已经循环完毕了,所以最终会返回5
            } 
            return arr;
        }
        console.log(box()[0]()); //5
        console.log(box()[1]()) //5

文章头部例子的执行过程

相信你们通过学习闭包的原理已经知道为什么输出上面的结果了那么我们看看如何解决闭包问题吧

  • let定义
function box() {
            var arr = [];
            for (let i = 0; i < 5; i++) {
                debugger;
                arr[i] = function () {
                    return i; 
                }
            
            } 
            return arr;
        }
        console.log(box()[0]()); //0
        console.log(box()[1]()) //1
  • 立即执行函数
  • 两种形式*
( function (){}() );    //w3c 建议第一种
( function (){})(); //只有表达式才能被执行
      function box() {
            var arr = [];
            for (var i = 0; i < 5; i++) {
                debugger;
                arr[i] = (function () {
                    return i; 
                })()
            
            } 
            return arr;
        }
        console.log(box()[0]); //0
        console.log(box()[1]) //1

使用立即执行函数处理后,arr[]数组存的数据是function(){}函数return i的结果啦!!!

  • 匿名函数
function box() {
            var arr = [];
            for (var i = 0; i < 5; i++) {
                debugger;
                arr[i] = (function (i) {
                    return i; 
                })(i)
            
            } 
            return arr;
        }
        console.log(box()[0]); //0
        console.log(box()[1]) //1

匿名函数与立即执行函数的过程和结果一致。

  • forEach()
function box() {
            var arr = [];
            [0, 1, 2, 3, 4, 5].forEach(
                function (i) {
                    arr[i] = function () {
                        debugger;
                        return i;
                      };
                }
            )
            return arr;
        }
        console.log(box()[0]()); //0
        console.log(box()[1]()) //1

如何arr[]数组元素比较多不建议用...

闭包的作用

1.实现公有变量
2.可以做缓存(存储结构)
3.可以实现封装,属性私有化
4.模块化开发,防止污染全局变量

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容