关于闭包的理解,我们看到过很多种说法:
- 内部函数引用内部变量并被外部所引用称之为闭包;
- 函数在定义时的词法作用域以外的地方被调用就会形成闭包;
- 闭包就是指有权访问另一个函数作用域中的变量的函数;
- 闭包可以延长作用域链;
- ...
看过这么多解释以后,我们可能都明白:“通过闭包,我们可以访问到其他作用域中的变量”。但是至于为什么,好像并没有完全理解。
最近在学习闭包的过程中,看到一篇很好的文章(需要翻墙访问),此文的末尾有这么一段话:
The way I will always remember closures is through the backpack
analogy. When a function gets created and passed around or returned
from another function, it carries a backpack with it. And in the
backpack are all the variables that were in scope when the function
was declared.
‘我理解闭包的方式是通过背包进行类比。当函数被创建并传递或者从其他函数
返回时,会携带一个背包。并且在这个背包中装有的是函数声明时所在作用
域中的所有变量。’
上段话中,有几点很重要的点:函数声明、作用域、所有变量,我们接下来对闭包的分析也会围绕这几个关键词展开。
“背包” --> 函数声明时所在作用域中的所有变量
在具体理解闭包之前,我们先理清楚下面代码的执行逻辑(清楚的可以自行跳过):
var x = 10;
function fn(){
console.log(x); // 10?20?
}
function show(callback){
var x =20;
(function (){
callback();
})();
}
show(fn);
以上代码执行的结果是10还是20呢?我们从定义和执行两个阶段分析一下:
定义时各作用域中可访问的变量:
- 全局作用域:x、fn、show
- fn函数作用域:可访问所在作用域中的所有变量:x和show
- show函数作用域:x、可访问所在作用域中的所有变量:x和fn
执行时:
- show函数创建新的执行上下文 --> 进执行栈(作用域链:show-->window),执行函数fn
- fn函数创建新的执行上下文--> 进执行栈(作用域链fn-->window),执行代码console.log(x):fn发现自己的函数作用域中没有x,因此沿着作用域链查找发现x。因此最终执行结果是10,而不是20。
理解清楚上面代码的执行结果,我们对上述代码稍作修改,继续分析:
var x = 10;
function fn(){
return x;
}
function show(){
var x = 20;
return function (){
console.log(x+fn()); // 20?30? 40?
}
}
var a = show();
a(); // show函数执行结束,变量都会清空,因此无法访问其中的变量x
这段代码的执行结果只与x和函数fn的返回值相关,因此:
- 函数fn的返回值是多少呢?这个问题可能看过一开始的例子,就会知道答案是10;
- “函数show执行结束,变量都会清空,其中的x=20这个变量应该就无法访问”这句话乍一看很有道理,但实际因为闭包的存在,恰恰相反x刚好就是20;
那么新的问题就会产生了:“为什么闭包的存在就可以访问到x=20这个变量,这其中的原理是什么呢?”
带着疑问,我们慢慢来分析上述代码的执行过程:
定义时各作用域中可访问的变量:
- 全局作用域:x、fn、show、a
- fn函数作用域:全局作用域中的x、a和函数show
- show函数作用域:x、全局作用域中的x、a和函数fn
执行时: - 执行函数show,创建新的执行上下文并入执行栈,返回匿名函数的定义和它的闭包给变量a(注意:不仅返回函数定义,还返回了函数的闭包,而“背包”中装的恰恰就是函数声明时所在作用域中的所有变量:这里仅有x --> 20)。show执行上下文从执行栈中推出;
- 执行函数a,创建新的执行上下文并入执行栈,发现新的函数fn,继续;
- 执行函数fn,创建新的执行上下文并入执行栈,根据其作用域链fn-->window,将x的值10返回(如果不理解为什么是10,再去看下本文开始的代码示例分析)。fn执行上下文从执行栈中推出;
- 继续执行函数a,a中并没有变量x的定义,但因为闭包的存在,此时a会先去闭包中查找,发现闭包中有x,因此执行打印结果30;如果闭包中没有,则会延着作用域链继续向上查找。
代码分析结束,疑问也就此解答:(“为什么闭包的存在就可以访问到x=20这个变量,这其中的原理是什么呢?”)
- 因为返回函数时,不仅返回函数的定义,还会返回函数声明时所在作用域中的所有变量,这些变量由闭包承载;
- 返回的函数被调用时,对于其中变量的访问:会首先去它的闭包中查找,然后是父级作用域,直到全局作用域;
- 因此,返回的函数可以访问到其声明时所在作用域中的变量。
闭包 -- “通过闭包可以访问其他作用域中的变量” 原理你懂吗?
虽然上面的分析看起来内容很多很复杂,但其实把闭包理解成是“背包”,就会很容易理解闭包的作用和其原理:
- 作用:通过闭包可以访问其他作用域中的变量
- 原理:为什么通过闭包可以访问其他作用域中的变量?
因为函数返回时会返回函数定义和它的闭包。而闭包中“承载”的是函数声明时所在作用域中的所有变量。因此我们可以通过闭包访问其他作用域中的变量。
最后,我们再分析一个例子,看看你是否真的懂得了闭包 ~ 背包:
function example(){
var count = 1;
return function (){
count += 1;
console.log(count);
}
}
var increase = example();
var r1 = increase(); // ?
var r2 = increase(); // ?
var r3 = increase(); // ?
- 执行example函数,返回匿名函数的定义和它的闭包(count = 1);
- 执行increase函数,首先在闭包中找到count的值为1,将count加1,并将新的结果2赋值给count;
- 执行increase函数,首先在闭包中找到count的值为2,将count加1,并将新的结果3赋值给count;
- 执行increase函数,首先在闭包中找到count的值为3,将count加1,并将新的结果4赋值给count;
- 因此打印的结果分别是2,3,4。
看完此篇文章,我希望你也会和我一样有所启发,可以从“背包”的角度去理解闭包,可以在理解闭包的原理时想到下面这两句话:
- 当函数被创建并传递//或者函数被返回时,不仅返回该函数的定义,还包括它的闭包;
- 闭包“承载”的就是函数声明时所在作用域中的所有变量。
关于闭包的应用场景,本文暂不分析,持续学习再持续更新。