《你不知道的Javascript(上)》阅读笔记(二)

我又来了,隔了这么久才写第二篇,拖延症的我~~~

闭包

定义

闭包是个老生常谈的话题了,网上也有一大堆相关的文章,不过既然是笔记,那也简单提一下吧。
先看wiki中闭包的定义:

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

看定义,有两个重点,自由变量和函数。那什么是自由变量?
先看个最简单的闭包例子:

1
2
3
4
5
6
7
8
function foo(){
var a = 2;
return function bar(){
console.log(a)
};
}
var baz = foo();
baz();//2

想必你也在各种文章看到过类似的例子,那我们就把它往定义中套一套。
我在上篇阅读笔记中说过,内层作用域可以访问到外层作用域的变量。没错,这个bar访问到的外部变量a,相对于bar来说就是一个自由变量。这其实也就满足了最广义的闭包定义了,但我们js中通俗意义上的闭包是还要满足后面这句

这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

离开了创造它的环境。是的,经过foo那条语句的执行,bar已经被返回并赋值到baz中,也就是暴露在了全局作用域中,离开了创造它的环境。
这个被引用的自由变量将和这个函数一同存在。也就是说,变量a(连同整个内部作用域)并不会因为foo被执行完了就消失,而是会保留在内存中。
就像下面的例子

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

bar中执行了a++,你会看到,每次运行baza的值是累加的,而不是2

综上,这个baz就是我们通常所说的闭包了。

常见闭包

一个很常见的例子就是在for循环里使用闭包
如下面

1
2
3
4
5
for(var i= 1; i <= 5; i++){
setTimeout( function timer(){
console.log(i);
},i*1000);
}

你可会以为这段代码是以一秒的间隔输出1~5
但实际上,这段代码在运行时会以每秒一次的频率输出五次6
6是怎么来的?在循环结束后,i的值为6。也就是说,timer里面的打印i的在循环结束后的i
仔细想想也的确如此,setTimeout的回调是在循环结束后才调用的,我们期望每次循环中会有一个i的副本被保存timer中,以至于在后面输出,但事实上for循环并没有提供这样的机制,所以每次输出的i都是在循环结束后的值6i是存在于全局作用域中被共享的。

这个时候可以用闭包解决

1
2
3
4
5
6
7
8
for(var i= 1; i <= 5; i++){
(function(){
var j = i;
setTimeout( function timer(){
console.log(j);
},j*1000);
})();
}

或者更常见的写法是这样

1
2
3
4
5
6
7
for(var i= 1; i <= 5; i++){
(function(i){
setTimeout( function timer(){
console.log(i);
},i*1000);
})(i);
}

我们用一个立即执行的匿名函数来构造一个封闭的内部作用域,复制一个i的副本,与timer一起构成一个闭包,从而达到每次保存i的值的目的。
这便是闭包的常见用法。

PS:ES6中,我们可以直接用let来代替var,生成块级作用域,达到同样的效果而不用闭包。

总结

无论你懂不懂闭包,我想你的代码中已经有意无意地存在了闭包。
闭包作用有好多,变量封装,私有化;函数嵌套;函数可以很方便地访问到外部变量等。但不合理的应用也会很容易造成内存泄漏,代码混乱等问题。
重点是要理解闭包及其背后的作用域机制,这样才能更好用闭包来为我们服务。