前言
本篇是基于对 《你不知道的JavaScript(上卷)》中的第五章的总结理解。
不完全正确的概念
简单通俗的说,函数内嵌函数就是闭包。但不完全正确,最重要的是内部函数执行时仍然持有外层作用域内的引用。
闭包解释
function outer() { var a = 2 function inner(){ console.log(a) } return inner } var fun = outer() fun()
上述代码就是一个闭包。
- 内部函数 inner 的作用域包括自身作用域外还向外涵盖,所以 inner 内可以使用 outer 内的变量 a;
- outer 函数将 inner 函数作为返回值。
一般来说,outer 函数执行完后,内部的变量 a 本来要被垃圾回收,但是其内部的函数 inner 有在使用 变量 a,为了 fun(也就是 inner ) 能够何时何地的正确执行,所以变量 a 不会被回收(保留了outer内的作用域)。这就是闭包。再看:
var fun function outer() { var a = 2 function inner(){ console.log(a) } fun = inner } outer() fun()
上述代码也是闭包。将内部函数 inner 赋值给fun。为了 inner 能够正确执行,变量 a 没有被回收。
所有,无论使用何种方法将内部函数传递到外层作用域,内部函数仍然保持着原始作用域的引用(没有被回收),就会产生闭包。
再看其他:
function wait(num) { setTimeout(function timer(){ console.log(num) }, 1000) } wait(1)
这也是闭包,不管执行1秒后,timer 内部仍然引用着num, 因为 setTimeout 会持有对 timer 的引用,并没有 clear。所以 wait 内的作用域会保留下来。
function click(value) { $(selector).click(function(){ alert(value) }) } click('hello')
这也是闭包,因为 $(selector).click 绑定的函数内持有 click 的value,而 $(selector).click 执行后并没有解绑,所以为了保证正确运行,会保留 click 内的作用域。
function outer () { var a = 2 function inner () { console.log(a) } inner() } outer()
上述代码,虽然函数内嵌了函数,但严格来讲并不是闭包。inner 执行后,并没有持有变量 a 的引用。outer 内的作用域可能已经被回收了。
经典问题
for (var i=1;i<=5;i++) { setTimeout(function timer(){ console.log(i) }, i ) }
上述代码会打印 5 次 6。
首先,javascript是单线程的,所以setTimeout(异步)会等for循环完了才会执行,而 for 循环完时, i 的值是6。但这并不足以导致setTimeout每次都打印出 6 。
更重要的是每个(5个)setTimeout对 i 的引用,因为 for 中使用 var i 声明,所以 i 是在全局作用域内,每个setTimeout都引用了同一个 i。
所以才会每次都打印出 6。
相当于:
var i = 6 setTimeout(function timer(){ console.log(i) }, i ) setTimeout(function timer(){ console.log(i) }, i ) setTimeout(function timer(){ console.log(i) }, i ) // .....
-
解决方案
只要隔离每个setTimeout对 i 的引用即可。
for (var i=1;i<=5;i++) { (function(){ var j = i setTimeout(function timer(){ console.log(j) }, j ) })() } ////////////////////////////////// for (var i=1;i<=5;i++) { (function(j){ setTimeout(function timer(){ console.log(j) }, j ) })(i) } ////////////////////////////////// for (let i=1;i<=5;i++) { setTimeout(function timer(){ console.log(i) }, i ) }
上面三种本质都是隔离每个setTimeout对 i 的引用。IIFE内自成一个作用域,将 i 赋值给其他变量,就隔离了对 i 的引用了。
而使用 let,虽然 let 没有用在 {...}内,但 let 在for头部中有特殊行为,这个行为会将 i 多次声明,每迭代一次,声明一次,每次迭代都会使用上一次迭代结束时的值来初始化 i 。
相当于:
// 这里块作用域指 IIFE 内的作用域 { var j = 1 setTimeout(function timer(){ console.log(j) }, j ) } // 这里块作用域指 IIFE 内的作用域 { var j = 2 setTimeout(function timer(){ console.log(j) }, j ) } // ...... ///////////////////////////////// { let i = 1 setTimeout(function timer(){ console.log(i) }, i ) } { let i = 2 setTimeout(function timer(){ console.log(i) }, i ) } // ......