JavaScript学习笔记---理解闭包

初探JavaScript closure

JavaScript closure,“closure”翻译为闭包,闭包的官方解释是这样的:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
官方的解释真的是晦涩难懂,我们还是来看下例子吧。

1
2
3
4
5
6
7
8
9
10
/*一个简单的闭包demo*/
function a() { //a的块级作用域
function b(){ //a的局部作用域,b的块级作用域,一般只能在a中被调用
console.log("Hello closure!");
};
return b;
}
//b(); /*如果不通过闭包的话,这样是不能取得函数a中的变量的, b报错undefined*/
var c = a();
c(); // Hello closure! ==> a()();

分析:首先声明了一个函数a,然后在函数中再声明函数b,并在函数a中返回函数b的值,即a取得了函数b的引用,这样我们就可以在函数a外面通过引用函数a中b的引用来取得函数b的结果了。
通过上述例子外面可以了解到,使用闭包我们可以在函数的外部访问到函数内部的局部变量或函数。

使用闭包后的一些分析

在MDN中的闭包模块中提到:定义在闭包中的函数可以“记忆”它创建时候的环境。这就是使用闭包的好处,同样也是其出现bug,或者造成内存泄露的原因所在。
先看下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*#1 实现累加计数*/
function a(){
var count = 0;
function b(){
count ++;
console.log(count);
}
return b;
}
var c = a();
c(); //1
c(); //2
c(); //3

分析: 只要存在外部变量对函数b的引用, 变量count就会一直存在函数a的环境中, 因为a存在对b的引用, 可以说函数b 依赖于函数a而存在,那么函数a中的变量也存在。理解这句之前我们要先对JavaScript的垃圾回收机制 有所了解:在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。我们接下来说到闭包的内存泄露也是因为JavaScript的垃圾回收机制(引用计数回收,”0”引用回收)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*#2.1 在循环中创建闭包,非常常见的一个bug*/
function a() {
var b = new Array();
for (var i=0; i < 10; i++){
b[i] = function() {
return i;
};
}
return b;
}
var c = a();
for (var i = 0; i < 10; i++) {
console.log("c["+i+"]"+": "+c[i]()+"\n");
}

#2.1 分析:返回的数组b的值都是 10 闭包只能取得包含函数中任何变量的最后一个值,因为它都是对函数a中的同一个局部变量 i 的引用。
解决办法: 创建一个每次循环都会执行一次的匿名函数:将每次循环时包围函数的i值作为参数,存入匿名函数中。因为函数参数是按值传递的,而非引用,所以每个匿名函数中的num值 都为每此循环时i值的一个副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*#2.2  解决方案*/
function a() {
var b = new Array();
for (var i=0; i < 10; i++){
b[i] = function(num) {
return function() {
return num;
};
}(i);
}
return b;
}
var c = a();
for (var i = 0; i < 10; i++) {
console.log("c["+i+"]"+": "+ c[i]() +"\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*#3 使用闭包,内存泄露的经典案例*/
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
// 'unused' 是唯一引用了 'originalThing' 的地方 但是 'unused' 永远不会被调用
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个 1MB 大小的对象
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000); //每一秒就会消耗1MB内存,除非关闭浏览器

 分析:每个 theThing 对象中包含 1MB 的 longStr 对象,当我们调用 replaceThing 方法时,它将会在 originalThing 中保存着上一个 theThing 的引用。 但是我们仍然认为这样不会造成问题。因为每次调用的时候,上次的引用 originalThing 将会被反向引用(当 originalThing 通过 originalThing = the Thing 被重置)。此外,只有 replaceThing 的主体被引用了, unused 函数 从来没有被引用。

  那么为什么会造成内存泄露呢? 闭包实现的一个最典型的特点是:每个函数对象都有一个字典类型的链接对象来代表它的词法作用域。如果在replaceThing中定义两个都使用originalThing的函数,很重要的一点是他们获取到的是同一个对象,即使originalThing被多次的分配,所以这两个函数共享相同的词法环境。但只要一个变量被任意一个闭包函数使用,那么它最终还是会存在于在这范围内的所有闭包函数共享的词法环境中。而也正是这一细微的差别导致了令人困惑不已的内存泄漏问题。

 更多关于闭包使用中出现的例子可以去看Tom大叔的 深入理解JavaScript系列-闭包(Closures)

使用闭包的场景

  1. 保护函数内的变量安全。以#1例子为例,函数a中count只有函数b才能访问,而无法通过其他途径访问到,因此保护了函数a中count只有函数b才能访问的安全性。
  2. 在内存中维持一个变量。依然如前例,由于闭包,函数a中count的一直存在于内存中,因此每次执行c(),都会给count自加1。
  3. 通过保护变量的安全实现JavaScript私有属性和私有方法(不能被外部访问)。

使用闭包的性能考量

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。

考虑以下虽然不切实际但却说明问题的示例:

1
2
3
4
5
6
7
8
9
10
11
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};

this.getMessage = function() {
return this.message;
};
}

上面的代码并未利用到闭包的益处,因此,应该修改为如下常规形式:

1
2
3
4
5
6
7
8
9
10
11
12
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};

或者改写为:

1
2
3
4
5
6
7
8
9
10
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};






参考资料:Mozilla 开发者网络->JavaScript闭包

文章目录
  1. 1. 初探JavaScript closure
  2. 2. 使用闭包后的一些分析
  3. 3. 使用闭包的场景
  4. 4. 使用闭包的性能考量