闭包

对于 JavaScript 的程序员来说,闭包是一个难懂又必须征服的概念。在介绍闭包前,先来看看和闭包息息相关的变量作用域和变量的生存周期。

变量的作用域

变量的作用域无非就是两种:全局变量和局部变量。当在函数中声明一个变量的时候,如果用关键字 var 来声明此变量,那么它就是局部变量,如果没有 var 那么就会成为全局变量。一般不建议用这种方式定义全局变量。而是用 var 将变量声明在函数的外面。

1
2
3
4
5
var name = "webape.net"; // 全局变量
function func() {
var name = "webape"; // 局部变量
age = 12; // 全局变量,建议少用这种全局变量的定义方式
}

JavaScript 中,函数可以用来创造函数作用域。函数内部可以看到外面的变量,而外面看不到函数里的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,直到搜索到全局变量为止。

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1;
var func1 = function () {
var b = 2;
var func2 = function () {
var c = 3;
console.log(b); // 2
console.log(a); // 1
};
func2();
console.log(c); // 报错:c is not defined
};
func1();

那么能不能从函数外部来访问局部变量呢?答案是可以。

1
2
3
4
5
6
7
var func = function () {
var number = 6;
return function () {
return number;
};
};
console.log(func()()); // 6

变量的生存周期

全局变量的生存周期是永久的,除非我们主动的销毁这个全局变量。而对于局部变量来说,当退出函数时,这些局部变量就失去了生存的环境,所以会随着函数调用的结束而被销毁。
那么是否可以让局部变量在函数调用结束的时候不被销毁呢?来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
var func = function () {
var a = 1;
return function () {
a++;
console.log(a);
};
};
var f = func();
f(); // 2
f(); // 3
f(); // 4

从上面这个输出结果,我们可以看到局部变量 a 并没有随着 f() 的调用而被销毁。这是因为全局变量 f 保存着对 func 里面匿名函数的引用。而这个匿名函数是可以访问到局部变量 a 的,所以自然 a 也被保存下来了。所以这种情况下局部变量的生存周期就得以延续。上面这个匿名函数其实就是一个闭包。再来看一个闭包的经典应用。

1
2
3
4
5
6
7
// 假设有5个div
var nodes = document.getElementsByTagName("div");
for (var i = 0, l = nodes.length; i < l; i++) {
nodes[i].onclick = function () {
console.log(i);
};
}

当真正执行这段代码的时候,发现无论点击那个 div,最后打印的结果都是 4。这是因为 onclick 事件是异步触发的,当事件触发的时候,for 循环早已经结束,此时变量 i 的值已经是 4。解决方案可以是用闭包把每次的 i 都保存起来:

1
2
3
4
5
6
7
for (var i = 0, l = nodes.length; i < l; i++) {
(function (i) {
nodes[i].onclick = function () {
console.log(i);
};
})(i);
}

同理,我们编写一段代码用来判断对象类型:

1
2
3
4
5
6
7
8
9
10
11
12
var Type = {};
for (var i = 0, type; (type = ["String", "Array", "Number"][i++]); ) {
(function (type) {
Type["is" + type] = function (obj) {
return (
Object.prototype.toString.call(obj) === "[object " + type + "]"
);
};
})(type);
}
Type.isArray([]); // true
Type.isString("str"); // true

什么是闭包

官方的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因为这些变量也是该表达式的一部分。这个概念不好理解,我们可以简单得理解成闭包就是能够访问函数内部变量的函数。

闭包的作用

  • 可以在函数外部访问在函数内部定义的局部变量

  • 延续局部变量的生存周期

  • 封装变量
    闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    var mult = function () {
    var a = 1;
    for (var i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
    }
    return a;
    };
    mult(2, 3, 4); // 24

mult 函数接受一些 Number 类型的参数,并且返回他们的乘积。现再我们觉得对于那些相同参数来说,每次再进行计算是一种浪费,所以我们决定加入缓存机制来提高函数性能:

1
2
3
4
5
6
7
8
9
10
11
12
var cache = {};
var mult = function () {
var args = Array.prototype.join.call(arguments, "");
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return (cache[args] = a);
};

我们看到 cache 这个变量仅仅在 mult 函数中被使用,所以为了避免 cache 污染全局变量,我们将它放到 mult 函数里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var mult = (function () {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, "");
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return (cache[args] = a);
};
})();

提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有利于代码的复用,如果这些小函数有一个良好的命名,那么它们本身就起到了一个很好的注释作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mult = (function () {
var cache = {};
var calculate = function () {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
};
return function () {
var args = Array.prototype.join.call(arguments, "");
if (cache[args]) {
return cache[args];
}
return (cache[args] = calculate.apply(null, arguments));
};
})();

闭包与内存管理

局部变量本来应该在函数退出的时候就被结束引用,但如果局部变量被封装在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上来看,闭包确实会使一些数据无法被及时销毁。如果将来需要回收这些变量,我们可以手动把这些变量设为 null
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域中保存着一些 DOM 节点,这个时候就有可能造成内存泄露。在 IE9 之前的浏览器中,由于 BOMDOM 中的对象是使用 C++ 对象以 COM 对象的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数的垃圾收集机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收。但我们只要把循环引用中的对象设为 null 即可解决这个问题。