Javascript之深入理解闭包
闭包算是js里面比较不容易理解的点,尤其是对于没有编程基础的人来说。
其实闭包要注意的就那么几条,如果你都明白了那么征服它并不是什么难事儿。下面就让我们来谈一谈闭包的一些基本原理。
闭包的概念
一个闭包就是一个函数和被创建的函数中的作用域对象的组合。(作用域对象下面会说)
通俗一点的就是 “ 只要一个函数中嵌套了一个或多个函数,那么我们就可以称它们构成了闭包。 ”
类似这样:
1 function A() {
2 var i = 5;
3 return function() {
4 console.log('i = '+i);
5 }
6 }
7
8 var a = A();
9 a(); // i = 5
闭包的原理
1、外部函数的局部变量若会被闭包函数调用就不会在外部函数执行完毕之后立即被回收。
我们知道,不管什么语言,操作系统都会存在一个垃圾回收机制,将多余分配的空间回收掉以便减小内存。而一个函数的生命周期的是从调用它开始的,在函数调用完毕的时候函数内部的局部变量等都会被回收机制回收。
我们拿上述例子来说,当我们的外部函数A调用完毕时,A中的局部变量i按理说就会被操作系统回收而不存在,但是当我们用了闭包结果就不是那样了,i并不会被回收。试想,如果i被回收了那么返回的函数里面岂不是就是打印undefined了?
i为什么没有被回收?
在javascript执行一个函数的时候都会创建一个作用域对象,将函数中的局部变量(函数的形参也是局部变量)保存进去,伴随着那些传入函数的变量一起被初始化。
所以当调用A的时候就创建了一个作用域对象,我们姑且称之为Aa,那么这个Aa应该是这样的: Aa = { i: 5 }; 在A函数返回一个函数之后,A执行完毕。Aa对象本应该被回收,但是由于返回的函数使用了Aa的属性i,所以返回的函数保存了一个指向Aa的引用,所以Aa不会被回收。
所以理解作用域对象,就能理解为什么函数的局部变量在遇到闭包的时候不会在函数调用完毕时立即被回收了。
再来个例子:
1 function A(age) {
2 var name = 'wind';
3 var sayHello = function() {
4 console.log('hello, '+name+', you are '+age+' years old!');
5 };
6 return sayHello;
7 }
8 var wind = A(20);
9 wind(); // hello, wind, you are 20 years old!
你能说出的它的作用域对象Ww是什么吗?
Ww = { age: 20, name: 'wind' };
2、每调用一次外部函数就产生一个新的闭包,以前的闭包依旧存在且互不影响。
3、同一个闭包会保留上一次的状态,当它被再次调用时会在上一次的基础上进行。
每调用一次外部函数产生的作用域对象都不一样,你可以这样想,上面的例子,你每次传入的参数age不一样,所以就每次生成的对象不一样。
每调用一次外部函数那么就会生成一个新的作用域对象。
1 function A() {
2 var num = 42;
3 return function() { console.log(num++); }
4 }
5 var a = A();
6 a(); // 42
7 a(); // 43
8
9 var b = A(); // 重新调用A(),形成新闭包
10 b(); // 42
这个代码让我们发现了两个事情,一、当我们连续调用两次a();,num会在原基础上自加。说明同一个闭包会保留上一次的状态,当它被再次调用时会在上一次的基础上进行。 二、我们的b();的结果为42,说明它是一个新的闭包,并且不受其他闭包的影响。
我们可以这样想,就好比我们吹肥皂泡一样,我每次吹一下(调用外部函数),就会产生一个新的肥皂泡(闭包),多个肥皂泡可以同时存在且两个肥皂泡之间不会相互影响。
4、在外部函数中存在的多个函数 “ 同生共死 ”
以下三个函数被同时声明并且都可以对作用域对象的属性(局部变量)进行访问与操作。
var fun1, fun2, fun3;
function A() {
var num = 42;
fun1 = function() { console.log(num); }
fun2 = function() { num++; }
fun3 = function() { num--; }
}
A();
fun1(); // 42
fun2();
fun2();
fun1(); // 44
fun3();
fun1(); //43
var old = fun1;
A();
fun1(); // 42
old(); // 43 上一个闭包的fun1()
由于函数不能有多个返回值,所以我用了全局变量。我们再次可以看出在我们第二次调用A()时产生了一个新的闭包。
当闭包遇到循环变量
当我们说到闭包就不得不说当闭包遇到循环变量这一种情况,看如下代码:
1 function buildArr(arr) {
2 var result = [];
3 for (var i = 0; i < arr.length; i++) {
4 var item = 'item' + i;
5 result.push( function() {console.log(item + ' ' + arr[i])} );
6 }
7 return result;
8 }
9
10 var fnlist = buildArr([1,2,3]);
11 fnlist[0](); // item2 undefined
12 fnlist[1](); // item2 undefined
13 fnlist[2](); // item2 undefined
怎么会这样呢?我们预想的三个输出应该是 item0 1, item1 2, item2 3。为什么结果却是返回的result数组里面存储了三个 item2 undefined ?
我们上文中提到过两点,1、闭包在返回的时候对作用域对象有一个引用。2、在外部函数中存在的多个内部函数 “ 同生共死 ”。
我们的for循环为外部函数创建了多个“同生共死”的内部函数,它们都共享一个环境,而当result数组返回的时候,所有的内部函数都引用了同一个作用域对象:
1 var bArr = {
2 item: 'item2',
3 i: 3,
4 arr: [1,2,3]
5 }
为什么作用域对象是这样的?拿我们上面的例子来说,当循环全部结束的时候作用域对象中的属性 i 正好是i++之后的3,而arr[3]是没有值的,所以为undefined。
有朋友会疑惑:为什么item的值是item2,难道不应该是item3吗?
注意,在最后一次循环的时候也就是 i = 2的时候,item的值为item2,当 i++,i = 3循环条件不满足循环结束,此时的item的值已经被保存下来了,所以此时的arr[i]为arr[3],而item为item2。这样能理解吗?
如果我们将代码改成这样那就说得通了:
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
result.push( function() {console.log('item' + i + ' ' + arr[i])} );
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[1](); // item3 undefined
那么问题来了,如何改正呢?且看代码:
1 function buildArr(arr) {
2 var result = [];
3 for (var i = 0; i < arr.length; i++) {
4 result.push( (function(n) {
5 return function() {
6 var item = 'item' + n;
7 console.log(item + ' ' + arr[n]);
8 }
9 })(i));
10 }
11 return result;
12 }
13
14 var fnlist = buildArr([1,2,3]);
15 fnlist[0](); // item0 1
16 fnlist[1](); // item1 2
17 fnlist[2](); // item2 3
我们可以用一个自执行函数将i绑定,这样i的每一个状态都会被存储,答案就和我们预期的一样了。
所以以后在使用闭包的时候遇到循环变量我们要习惯性的想到用自执行函数来绑定它。
=========================3月14日更新======================================================
关于上面的问题还有一个更简单的方法:
1 function buildArr(arr) {
2 var result = [];
3 for (let i = 0; i < arr.length; i++) {
4 let item = 'item' + i;
5 result.push( function() {console.log(item + ' ' + arr[i])} );
6 }
7 return result;
8 }
9
10 var fnlist = buildArr([1,2,3]);
11 fnlist[0](); // item0 1
这里使用了let代替var,let的好处是可以“模拟创建”块作用域,点到为止,有兴趣的朋友可以自行深入了解let。
以上就是我对闭包的理解,如果有什么意见或建议希望我们能在评论区多多交流。感谢,共勉。