一. 闭包的理论

首先必须了解以下几个概念:

执行环境

每调用一个函数时(执行函数时),系统会为该函数创建一个==封闭的局部的运行环境==,即该函数的执行环境。函数总是在自己的执行环境中执行,如读写局部变量、函数参数、运行内部逻辑。创建执行环境的过程包含了创建函数的作用域,函数也是在自己的作用域下执行的。从另一个角度说,每个函数执行环境都有一个作用域链,子函数的作用域链包括它的父函数的作用域链。关于作用域、作用域链请看下面。

作用域、作用域链、调用对象

函数作用域分为词法作用域和动态作用域。

==词法作用域==是函数定义时的作用域,即静态作用域。当一个函数定义时,他的词法作用域就确定了,词法作用域说明的是在函数结构的嵌套关系下,函数作用的范围。这个时候也就形成了该函数的作用域链。作用域链就是把这些具有嵌套层级关系的作用域串联起来。函数的内部[[scope]]属性指向了该作用域链。

==动态作用域==是函数调用执行时的作用域。当一个函数被调用时,首先将函数内部[[scope]]属性指向了函数的作用域链,然后会创建一个==调用对象==,并用该调用对象记录函数参数和函数的局部变量,将其置于作用域链顶部。动态作用域就是通过把该调用对象加到作用域链的顶部来创建的,此时的[[scope]]除了具有定义时的作用域链,还具有了调用时创建的调用对象。换句话说,执行环境下的作用域等于该函数定义时就确定的作用域链加上该函数刚刚创建的调用对象,从而也形成了新的作用域链。所以说是动态的作用域,并且作用域链也随之发生了变化。再看这里的作用域,其实是一个对象链,这些对象就是函数调用时创建的调用对象,以及他上面一层层的调用对象直到最上层的全局对象。

譬如全局环境下的函数A内嵌套了一个函数B,则该函数B的作用域链就是:函数B的作用域—>函数A的作用域—>全局window的作用域。++当函数B调用时,寻找某标识符,会按函数B的作用域—>函数A的作用域—>全局window的作用域去寻找,实际上是按函数B的调用对象—>函数A的调用对象—>全局对象这个顺序去寻找的++。也就是说当函数调用时,函数的作用域链实际上是调用对象链。

闭包

在动态执行环境中,数据实时地发生变化,==为了保持这些非持久型变量的值==,我们用闭包这种载体来存储这些动态数据(看完下面的应用就会很好的体会这句话)。闭包的定义:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包就是嵌套在函数里面的内部函数,并且该内部函数可以访问外部函数中声明的所有局部变量、参数和其他内部函数。当该内部函数在外部函数外被调用,就生成了闭包。(实际上任何函数都是全局作用域的内部函数,都能访问全局变量,所以都是window的闭包)

譬如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
function f(x) {
var a = 0;
a++;
x++;
var inner = function() {
return a + x;
}
return inner;
}
var test = f(1);
alert(test());
</script>

== 垃圾回收机制:如果某个对象不再被引用,该对象将被回收。==

再结合前面所讲的一些概念,在执行var test=f(1)时创建了f的调用对象,这里暂且记作obj,执行完后虽然退出了外部执行环境,但内部函数inner被外部函数f外面的一个变量test引用。

由于外部函数创建的调用对象obj有一个属性指向此内部函数,而现在这个内部函数又被引用,所以调用对象obj会继续存在,不会被垃圾回收器回收,其函数参数x和局部变量a都会在这个调用对象中得以维持。虽然调用对象不能被直接访问,但是该调用对象已成为内部函数作用域链中的一部分,可以被内部函数访问并修改,所以执行test()时,可以正确访问x和a。所以说, 当执行了外部函数时,生成了闭包,被引用的外部函数的变量将继续存在。

二. 闭包的应用

应用1:

这个是我在用js模拟排序算法过程遇到的问题。我要输出每一次插入排序后的数组,如果在循环中写成

1
setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);

会发现每次输出的都是最终排好序的数组,因为arr数组不会为你保留每次排序的状态值。为了保存会不断发生变化的数组值,我们用外面包裹一层函数来实现闭包,用闭包存储这个动态数据。下面用了2种方式实现闭包,一种是用参数存储数组的值,一种是用临时变量存储,后者必须要深拷贝。所有要通过闭包存储非持久型变量,均可以用临时变量或参数两种方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript">
var arr = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];
var $ = function(id) { return document.getElementById(id); }
var Sort = {
Insert: function() {
for (var i = 1; i < arr.length; i++) {
for (var j = 0; j < i; j++) {
if (arr[i] < arr[j]) {
arr[i] = [arr[j], arr[j] = arr[i]][0];
}
}
setTimeout((function() {
var m = [];
for (var j = 0; j < arr.length; j++) {
m[j] = arr[j];
}
return function() {
$("proc").innerHTML += m + "<br />";
}
})(), i * 500);
//or 写成下面这样也可以
/*
setTimeout((function(m) {
return function() {
$("proc").innerHTML += m + "<br />";
}
})(arr.join(",")), i * 500);
*/
}
return arr;
}
}
</script>
</head>
<body>
<div>
var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div>
<div>
<input type="button" value="插入排序" onclick="Sort.Insert();" />
</div>
Proc:<br />
<div id="proc">
</div>
</body>
</html>

应用2:

为每个

  • 结点绑定click事件弹出循环的索引值。起初写成

    1
    2
    3
    4
    5
    id.onclick = function(){
    alert(i);
    }

    id.onclick = function(){alert(i);}

    发现最终弹出的都是4,而不是想要的 1、2、3,因为循环完毕后i值变成了4。为了保存i的值,同样我们用闭包实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title></title>
    <script type="text/javascript">
    window.onload = function() {
    for (var i = 1; i < 4; i++) {
    var id = document.getElementById("a" + i);
    id.onclick = (function(i) {
    return function() {
    alert(i);
    }
    })(i);
    }
    }
    </script>
    </head>
    <body>
    <ul>
    <li id="a1">aa</li>
    <li id="a2">aa</li>
    <li id="a3">aa</li>
    </ul>
    </body>
    </html>

    应用3:

    下面的code是缓存的应用,catchNameArr。在匿名函数的调用对象中保存catch的值,返回的对象由于被CachedBox变量引用导致匿名函数的调用对象不会被回收,从而保持了catch的值。可以通过CachedBox.getCatch(“regionId”);来操作,若找不到regionId则从后台取,catchNameArr 主要是为了防止缓存过大。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <script type="text/javascript">
    var CachedBox = (function() {
    var cache = {}, catchNameArr = [], catchMax = 10000;
    return {
    getCatch: function(name) {
    if (name in cache) {
    return cache[name];
    }
    var value = GetDataFromBackend();
    cache[name] = value;
    catchNameArr.push(name);
    this.clearOldCatch();
    return value;
    },
    clearOldCatch: function() {
    if (catchNameArr.length > catchMax) {
    delete cache[catchNameArr.shift()];
    }
    }
    };
    })();
    </script>

    同理,也可以用这种思想实现自增长的ID。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script type="text/javascript">
    var GetId = (function() {
    var id = 0;
    return function() {
    return id++;
    }
    })();
    var newId1 = GetId();
    var newId2 = GetId();
    </script>

    应用4:

    用闭包实现程序的暂停执行功能,还蛮创意的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <input type="button" value="继续" onclick='st();'/>
    <script type="text/javascript">
    var st = (function() {
    alert(1);
    alert(2);
    return function() {
    alert(3);
    alert(4);
    }
    })();
    </script>

    看了上面的这些应用,再回到前面的一句话:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。这就是闭包的作用。也就说遇到需要存储动态变化的数据或将被回收的数据时,我们可以通过外面再包裹一层函数形成闭包来解决

    当然,闭包会导致很多外部函数的调用对象不能释放,滥用闭包会使得内存泄露,所以在频繁生成闭包的情景下我们要估计下他带来的副作用