本文主要由浅入深,研究几个问题
问题1:闭包是如何形成的?
1、都知道,闭包的典型特征就是函数里面返回函数,注意闭包不是一种函数,而是一种现象。它的作用是可以让内部函数访问到外部函数的变量,而外部无法访问内部的变量。
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
console.log('outer a',a)
return fn;
}
foo(); //输出outer a,1
foo()(); //输出inner a,2和outer a:1
2、分析下这段代码,内部函数fn需要使用变量a,当一个函数被执行时,会创建一个执行上下文(代码的执行环境),而a这个变量的声明并不在fn的执行环境中,所以只能在fn的词法作用域中去寻找a,此时我们可以理解为fn的词法作用域就是foo的作用域。
所以这边存在了一个现象:变量a是在foo的执行上下文中被创建的,但是fn却包裹着被改变量的引用,只要变量存在着引用,那么这个变量是无法被释放的,这就形成了闭包。
3、顺着上面的代码,再度执行foo()和foo()();
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
console.log('outer a',a)
return fn;
}
foo(); //输出outer a,1
foo()(); //输出inner a,2和outer a:1
foo(); //输出outer a,1
foo()(); //输出outer a,1
我们发现,打印出来的结果跟上次的完全一样。这很好解释,每次函数执行时,会创建一个执行上下文,这个执行上下文会被推到执行栈中,并且浏览器会给每个变量分配内存。执行完毕以后,内存回收,执行上下文出栈,下次执行继续重复上面的的过程。
变量a正常被GC回收。
4、什么时候变量a无法被回收呢?将代码稍微调整下,此处不研究outer a了,故将其去掉。
function foo(){
var a = 1;
function fn(){
a++;
console.log('inner a',a);
}
return fn;
}
var f = foo();
f(); //inner a,2
f(); //inner a,3
f(); //inner a,4
发现每执行一次f,变量a就会+1,仿佛被记忆了一样,这又是为何呢?跟上面的区别主要是在于一个是直接执行,一个是将函数赋值个一个变量来执行。为什么结果不同?
此处我给的解释是:var f = foo(); 这边的 f 实际上是一个全局变量,而在js变量回收规则里面,全局变量是不会被回收的。同时var f = foo()实际上创建了一个内部函数fn的引用,因此它可以访问到fn执行时所在的代码环境,而a一直存在于这个
环境里,就有了不被回收的理由。
问题2:来看看hooks里面过时的闭包
function create(i){ let value = 0; return function(){ value += i; console.log('value',value) const message = `The current value is ${value}`; return function logValue(){ console.log(message); } } }; var inc = create(1); var log = inc(); // value 1 inc(); // value 2 inc(); // value 3 log(); // The curent value is 1
可以看到异常了,inc执行了三次以后,value应该是3,但是最后执行log的时候打印出来的message显示的value却是1,此处成log为过时的闭包,message为过时的变量。OK,那value到底是多少呢?继续在logValue里面将value打印出来。
function create(i){ let value = 0; return function(){ value += i; console.log('value',value) const message = `The current value is ${value}`; return function logValue(){ console.log(value,message); } } }; var inc = create(1); var log = inc(); // value 1 inc(); // value 2 inc(); // value 3 log(); // 3 The curent value is 1
正应了上面所述,value并没有过时,过时的变量是message。
为什么message是过时的变量呢?对这段代码进行分析:
1)var inc = create(1);创建了一个变量inc,该变量返回的是create函数里面第一层的匿名函数的引用。匿名函数被执行时,创建了执行上下文,而value一直存在与这个执行上下文中。但message也在啊,为什么会过期呢?
2)value跟message虽然都存在于同一个执行上下文里面,但是它们的生命周期不一样,value不是在当前执行上下文被创建的,而是在其词法作用域即外部的create()函数里面被创建的,根据闭包形成的条件,变量value不被释放。
而message就不同了,每次调用这个匿名函数的时候,message都会被执行上下文重新创建,重新分配内存。
3)var log = inc();创建了一个变量log,该变量返回的是执行了一次inc以后的logValue函数的引用,此时value=1.后面inc()被调用了多次,value发生了变化.
4) 最后执行log()的时候,即执行了logValue(),此时的message不在当前执行上下文中,要去其词法作用域中寻找,而message所在的词法作用域即第一层匿名函数,早先赋值给了log,所以输出了一个过时的结果。
问题3:如何解决过时的闭包问题?
原理理解了就好办了,因为我们发现value并没有过时,所以调整下message的位置即可。
function create(i){ let value = 0; return function(){ value += i; console.log('value',value) return function logValue(){ const message = `The current value is ${value}`; console.log(value,message); } } }; var inc = create(1); var log = inc(); // value 1 inc(); // value 2 inc(); // value 3 log(); // 3 The curent value is 3
或者改变下var log = inc()的顺序,使用新的闭包。此处略。