2

本文旨在解释闭包里的微观世界

内容包含:值类型作用域闭包

JS当中所有的function都是闭包,一般说来,嵌套的function的闭包性更强。这也是我们平时接触和研究比较多的地方。

在进入本文的核心部分以前,首先来理解几个概念:

  • 值类型

    声明一个值类型变量,编译器会在栈上分配一个空间,这个空间对应着该值的类型变量,空间存储的就是这个变量的值。存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。

  • 引用类型

    引用类型的实例分配在堆(heap)上,新建一个引用类型的实例,得到的变量值对应的是该实例的内存分配地址。存储在堆(heap)中的对象,也就是说,存储在变量中的值是一个指针(point),其指向存储对象的位置。

    javascript//值类型
    var a="xl";
    var b=a;
    a="XL";
    console.log(b); //输出  "xl"
    
    //引用类型
    var a={name:"xl"};
    var b=a;
    a.name="XL";
    console.log(b.name);//输出 "XL"
    

区别就是值类型变量是可以直接访问栈(stack)中的值:

  • 在第一段代码中,将变量"a"赋值给"b",相当于在stack中也为"b"开辟了一个存储其值的空间,与存储变量"a"的存储空间是相互独立的,因此修改"a"的值,不会影响到“b”的值。
  • 在第二段代码中,"a","b"都获得的是对于存储在heap当中实例的引用,当“a”对其进行修改的时候,“b”的引用也会受到影响。

接下来的内容就是关于闭包的微观世界

javascript    function a(){
        var i=0;
        function b(){
            console.log(++i);
        }
        return b;
    }

    var c=a(); //函数a执行后返回函数b,并将函数b赋给c
    c();//输出 1

本来这个地方变量i是定义在函数a中,并不能被函数a的外部所访问,但是这个地方因为在a中定义了一个函数b,函数b中有对变量i的引用,因此当b被a返回后,变量c获得了对函数a中函数b的引用,因此i不会被GC回收,而是存在内存当中。

当在一个函数a里面定义另外一个函数b,函数b有对函数a中变量的引用,当函数a执行并返回函数b,将b赋给变量c时,这样就存在相互之间的引用关系,并形成了大家经常见到的闭包

我们进一步的分析:这一部分的内容包含了作用域作用域链部分的内容.

依然拿上面的例子来分析:

  • 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时所在的“环境”,如果a是一个全局函数,那么scope chain中只有window对象。

  • 当执行函数a的时候,a会进入相应的执行环境(excution context).

  • 在创建执行环境的过程中,首先会为a添加scope属性,即a的作用域,其值就为第一步的scope chain.即a.scope=a的作用域链。

  • 然后执行环境会创建一个活动对象(call object).活动对象也是一个拥有属性的对象。但它不具有原型而且不能直接通过javascript代码访问。创建完活动对象后,把活动对象添加到a的作用域的最顶端,此时a的作用域链包含2个对象:a的活动对象和window对象。

  • 下一步是在活动对象上添加一个arguments属性,它保存着调用a时所传递的参数。最后把所有函数a的形参以及定义的内部函数b添加到a的活动对象上。在这一步中,完成了函数b的定义,正如第一步,函数b的作用域链被设置为b被定义时所处的环境,即a的作用域
    到此,整个函数a从定义到执行的过程就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数的a,因此函数a在返回的时候不会被gc收回。

  • 当函数b执行的时候,同样会按上述步骤一样。执行时b的作用域里包含了3个对象:{b的活动对象}、{a的活动对象}、{window对象}

下面用2张图来表示整个过程:

图一展示了函数a定义过程是如何创建作用域链的

图片描述

图二展示了函数a执行过程产生的活动对象(call object)

图片描述

在这其中有个非常重要的内容就是函数的作用域是在定义函数的时候就已经确定,而不是在执行的时候确定。

具体内容参见:鸟哥:Javascript作用域和作用域链

再来看看我们在平时经常遇到的一段代码:

javascript    HTML部分:
        <div id="example">
            <span>1</span>
            <span>2</span>
            <span>3</span>
        </div>

    JS:

    var spanArr=document.getElementById("example").getElementsByTagName("span");
    for(var i=0;i<3;i++){
        spanArr[i].onclick=function(){
            console.log(i);
        }
    }
    //不管点击哪个<span>都会输出3
    //这是因为在内部的匿名函数中i是对于外部的i的引用。当for循环结束以后,i的值变为了3.那么匿名函数相应获得的引用值夜都变为了3.所以最后不管点击哪个<span>最后都会输出3.
    //所以遇到这种情况的时候一般处理方法是
    1.将变量i保存在每个span对象上。
    for(var i=0;i<3;i++){
        spanArr[i].i=i;
        spanArr[i].onclick=function(){
            console.log(i);
        }
    }
    2.加一层闭包
    for(var i=0;i<3;i++){
        (function(i){
            spanArr[i].onclick=function(){
                console.log(i);
            }
        })(i)
    }
    //当然还有其他的方法,这里不多述。

参考文章:

  1. 理解javascript的作用域和作用域链
  2. javascript闭包深入理解
  3. 理解javascript闭包

苹果小萝卜
5.1k 声望356 粉丝

Github: [链接]