闭包

JavaScript初学者对闭包闻虎色变,感觉玄之又玄,要谈我们不得不从一个经典错误谈起

<div id="divTest">
    <span>0</span> <span>1</span> <span>2</span> <span>3</span>
</div>
<div id="divTest2">
    <span>0</span> <span>1</span> <span>2</span> <span>3</span>
</div>

绑定每个spanclick事件,让鼠标点击span的时候alert出对应的index

   var spans = $("#divTest span");
for (var i = 0; i < spans.length; i++) {
   spans[i].onclick = function() {
       alert(i);
   }
}

很简单的功能可是却偏偏出错了,每次alert出的值都是4,所以然者何?

内部函数

所谓内部函数就是定义在函数内的函数,俗称函数嵌套

function outerFn () {
    function innerFn () {}
}

innerFn就是一个被包在outerFn作用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会导致一个JavaScript错误:

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        console.log("Inner function");
    }
}
innerFn();

不过在outerFn内部调用innerFn,则可以成功运行:

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        console.log("Inner function");
    }
    innerFn();
}
outerFn();

逃脱

JavaScript允许像传递任何类型的数据一样传递函数,也就是说JavaScript中的内部函数能够逃脱定义他们的外部函数。

全局变量

我们可以通过把内部函数定义为全局变量的方式,实现内部函数在外部调用

var globalVar;

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        console.log("Inner function");
    }
    globalVar = innerFn;
}
outerFn();
globalVar();

调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。

返回值

可以通过在父函数的返回值来获得内部函数引用

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        console.log("Inner function");
    }
    return innerFn;
}
var fnRef = outerFn();
fnRef();

这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。

闭包

这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间)。

说了半天总算和闭包有关系了,闭包是指有权限访问另一个函数作用域的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数,就是我们上面说的内部函数,所以刚才说的不是废话,也是闭包相关的

变量的作用域

内部函数也可以有自己的变量,这些变量都被限制在内部函数的作用域中:

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        var innerVar = 0;
        innerVar++;
        console.log("Inner function\t");
        console.log("innerVar = "+innerVar+"");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

每当通过引用或其它方式调用这个内部函数时,就会创建一个新的innerVar变量,然后加1,最后显示

全局变量

内部函数也可以像其他函数一样引用全局变量:

var globalVar = 0;

function outerFn() {
    console.log("Outer function");
    function innerFn() {
        globalVar++;
        console.log("Inner function\t");
        console.log("globalVar = " + globalVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

父函数变量

但是如果这个变量是父函数的局部变量又会怎样呢?因为内部函数会引用到父函数的作用域(有兴趣可以了解一下作用域链和活动对象的知识),内部函数也可以引用到这些变量

function outerFn() {
    var outerVar = 0;
    console.log("Outer function");
    function innerFn() {
        outerVar++;
        console.log("Inner function\t");
        console.log("outerVar = " + outerVar + "");
    }
    return innerFn;
}

var fnRef = outerFn();
fnRef();
fnRef();

var fnRef2 = outerFn();
fnRef2();
fnRef2();

我们看到的是前面两种情况合成的效果,通过每个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次函数调用的作用域创建并绑定了一个新的outerVar实例,两个计数器完全无关。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。这种情况下我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。

闭包之间的交互

当存在多个内部函数时,很可能出现意料之外的闭包。我们定义一个递增函数,这个函数的增量为2

function outerFn() {
    var outerVar = 0;
    console.log("Outer function");
    function innerFn1() {
        outerVar++;
        console.log("Inner function 1\t");
        console.log("outerVar = " + outerVar + "");
    }

    function innerFn2() {
        outerVar += 2;
        console.log("Inner function 2\t");
        console.log("outerVar = " + outerVar + "");
    }
    return { "fn1": innerFn1, "fn2": innerFn2 };
}

var fnRef = outerFn();
fnRef.fn1();
fnRef.fn2();
fnRef.fn1();

var fnRef2 = outerFn();
fnRef2.fn1();
fnRef2.fn2();
fnRef2.fn1();

我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数

innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,就为innerFn2设置了outerVar的新的起点值,反之亦然。

我们也看到对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了面向对象数据的专有性。

解惑

现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会alert 4了。

for (var i = 0; i < spans.length; i++) {
   spans[i].onclick = function() {
       alert(i);
   }
}

上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个span的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。

这样每次我们点击span的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。而第二种方式是使用了一个立即执行的函数又创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号就是调用了,这时候把i当参数传入,函数立即执行,num保存每次i的值。

var spans2 = $("#divTest2 span");
    $(document).ready(function() {
        for (var i = 0; i < spans2.length; i++) {
            (function(num) {
                spans2[i].onclick = function() {
                    alert(num);
                }
            })(i);
        }
    });

还不太明白的同学可以参考一下之前的闭包的公开课

results matching ""

    No results matching ""