闭包

前言

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.模块化开发,防止污染全局变量

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容