闭包的文章看了许多,直到看到了《你不知道的JS》中对闭包的阐述,才让我真正理解闭包。
由下面一个例子引出闭包的概念,
function foo() {var a = 2;function bar() {console.log(a)}return bar;}var baz = foo()baz() // 2
当我们执行baz()的时候,很神奇的,居然可以访问的function foo()内部的局部变量a。显然,变量a在自身的词法作用域外被访问。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为引擎由gc来释放不再使用的内存空间,执行过的foo()显然应该是被gc回收的对象。
然而,闭包神奇之处正式可以阻止这件事的发生,事实上,foo()的作用域依然存在,而访问这个作用域的正是bar函数本身。bar函数有对该作用域的引用,这个引用就是闭包。(引用是闭包,闭包不一定是引用)
总结一下:
当函数可以记住并访问所在的词法作用域(静态作用域),即使函数是在当前词法作用域之外执行,这时就产生了闭包。
case1
function foo() {var a = 2;function bar() {console.log(a) // 闭包}bar();}foo();/*************************************/var name = '葡萄';function eat() {console.log( name ); // 闭包}eat(); // '葡萄'
case2
function foo() {var a = 2;function baz() {console.log(a) // 2}bar(baz)}function bar(fn) {fn() // 闭包}foo();
注意,词法作用域是编译时根据书写位置决定的,不是在执行的时候决定的。在此例中,baz通过参数传递给bar函数中作为fn函数执行,此时baz执行是在其词法作用域之外,且能够访问baz定义时的词法作用域。因此产生了闭包。
case3
var fn;function foo() {var a = 2;function baz() {// console.log(a) // 2}fn = baz}function bar() {fn() // 闭包}foo();bar();
同理,baz执行时在其词法作用域之外,且能够访问baz定义时的词法作用域。
case4
function wait(message) {setTimeout(function () {console.log(message); // 闭包}, 1000)}wait("Hello, closure!")
首先,在seTimeout函数内部的回调函数访问了词法作用域上的message。其次,调用时机由于回调函数的控制反转是在其他地方调用的,因此是在该回调函数词法作用域之外执行。
由此可见,回调函数基本都是在使用闭包的特性。
case5
var a = 2;(function (){console.log(a) // 闭包}())
虽然不满足在词法作用域外调用,但是它满足了访问当前词法作用域的条件,因此产生了闭包。
case6
for (var i = 1; i <= 5; i++) {setTimeout(function () {console.log(i)}, i * 1000);}// 结果:// 6 6 6 6 6
分析:
关于定时器如何产生闭包之前已经分析过了。当回调函数执行的时候,此时的词法作用域中i的值已经是6.因此输出的全是6.
如何改进?
改进1:
为每一次迭代都单独创建一个闭包,通过立即执行函数创建一个函数作用域,在作用域内保存当前i的值。之后每轮迭代对应的回调函数执行时,在词法作用域内能够查找到当时保存的i值。
for (var i = 1; i <= 5; i++) {(function (j) {setTimeout(function () {console.log(j)}, i * 1000);})(i)}// 结果:// 1 2 3 4 5
改进2:
利用let的特性,在循环的每一轮迭代中会保存当前的副本。
for (let i = 1; i <= 5; i++) {setTimeout(function () {console.log(i)}, i * 1000);}// 结果:// 1 2 3 4 5
