应该怎么去理解闭包(一)
-
关于闭包,javacript应该是我们经常接触的一个概念。最近也看了不少博客,觉得写得都不够清晰,虽然写到了词法作用域,但没有深入进去,都是在绕着闭包概念,但是没有谈。
闭包
闭包官方的解释是: 函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。(引用自MDN)
我觉得写得有些抽象,所以想自己总结下,然后分享给大家。
首先我们应该先了解什么是词法环境。
词法环境
词法环境也称词法作用域与作用域相似,但不同,词法作用域是作用域的子集,词法作用域就是你将代码中的变量和块作用域写在哪里决定的。例如:
function foo(a){ var b = a*2; function bar(c){ console.log(a,b,c); } bar(b*3); } foo(2);//2,4,12
当运行console.log(a,b,c)时,bar()的内部作用域会依次向上找a,b,c,如果找到了就使用这个引用,如果后面还有相同的名称,则会引用最先找到的。也就是匹配第一个标识符后停下来,当你想用全局变量中的a时,你可以通过使用window.a来调用,但如果有被遮蔽的其他非全局变量的a时,无论如何也找不到。
无论函数在哪里被调用,也无论如何调用,词法作用域都只由函数声明的位置决定。
再次回到闭包
我认为闭包实质就是函数+词法环境引用,通俗来说就是函数把给他的这个词法作用域记住了,同时还能任意的访问,例子如下:
function foo(){ var a = 2; function bar(){ console.log( a ); } return bar; } var baz = foo(); baz();//此时这个就是闭包的效果 //输出2
函数baz()的词法作用域能够访问foo()函数中的内部作用域,然后将函数bar()本身作为一个值类型进行传递,这个例子中我们将bar所引用的函数对象本身当成返回值。
在foo()执行之后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。
但是在这个例子中,bar在自己定义的词法作用域以外的地方执行。
在foo()执行完后,通常foo()会销毁自己的作用域,原因是引擎的垃圾回收器,按道理来说,foo()函数的作用域在使用过后会被回收。
但是闭包阻止了回收,内部的作用域依然存在,所以没有被回收,为什么会存在,因为bar()本身在使用。
而由于bar()函数拥有foo()函数的内部作用域的闭包(函数+词法作用域的引用),使其作用域一直存活,以便bar()在之后的任何时候访问。
bar()依然持有对该作用域的引用,这个引用就叫闭包。
然后我们区别一下,看下下面的例子:
function foo(){ var a = 2; function bar(){ console.log( a );//2 } bar(); } foo();
看起来也输出2了,但这个算是闭包吗?
从定义上看起来是,严格来说并不是,看起来只是利用了词法作用域,bar函数内部向上寻找a,然后打印,从学术上看是闭包,bar函数拥有了涵盖foo函数 的作用域闭包,因为bar嵌套在foo内部,所以可以理解为简单的闭包。
但是这样不便于我们真正理解闭包机制,最方便观察闭包的方式就是第一种利用函数对象去调用,将函数作为返回值。或者还有一种方式,则将函数当成参数传参调用,例如:
function foo(){ var a = 2; function baz(){ console.log( a );//2 } bar(baz); } function bar(fn){ fn();//这也是闭包 } foo();
将内部函数baz传递给bar,当调用内部函数(fn)时,其涵盖的foo()内部作用域的闭包,就可以观察到了,因为能访问a。
总结来说,就是无论通过哪种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用到闭包。
同样,再看下下面的例子:
function wait(message){ setTimeout(function timer(){ console.log(message); },1000); } wait("Hello closure!");
timer这个内部函数传递给了setTimeout(..),timer具有涵盖wait(..)作用域的闭包,因此依然保有对变量message的引用。
而在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数或许叫fn或者fnc,或其他的名字,引擎调用这个函数,在例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。
这也是闭包,只要涉及回调函数都是闭包。
总结下闭包通常出现在哪里:
- 闭包通常以内部函数作为返回值,然后再次调用函数,来调用内部函数,产生闭包
- 或者将函数作为参数进行调用,也是调用函数,调用内部函数产生闭包
- 一般的函数内部嵌套函数,然后在外部函数调用内部函数,尽管不利于观察,依然是闭包
- 只要涉及回调函数,那么就是闭包