简单易懂讲闭包
刚看到了 Rust 语言中利用闭包实现匿名函数,想着博客许久没更新了,那就写一篇帮助大家理解吧。闭包是 JavaScript 的一个特点,搜索引擎搜索闭包
基本都会用 JavaScript 做例子,同时闭包也是 JavaScript 初学者较难理解的一个部分。
前置知识
作用域规则
1 | const variable = "variable"; |
这段代码说明的是 JavaScript 的作用域链规则,readVar
函数执行时没有在自身作用域内找到variable
变量,于是向上层作用域寻找,并使用它。简单来说就是子作用域能使用父作用域的变量,而父作用域不能使用子作用域的变量。
讲清楚什么是闭包
一句话
闭包保存的是函数和函数运行的必要环境(使用到的变量等),它和作用域类似,但闭包只是包含一部分作用域,它是函数和函数使用的外部变量的集合。
产生
闭包在函数创建时就会产生,只要使用函数就存在闭包。
利用闭包
按照上面所述,只要有函数就有闭包,但是我们在使用的时候感受不到闭包的存在。要怎么利用闭包呢?
这个例子已经被用烂了,但是用它来讲闭包也再合适不过了。
1 | const countUp = (() => { |
第 4 行使用了一个父作用域的变量count
,可以改变count
的值,第 3 行将一个匿名函数返回到countUp
外部,相当于创建了一个快照并保存到立即执行函数外部,里边包含了函数内部环境和该函数使用的外部变量count
,这样通过闭包就能用countUp
函数控制count
值。你可以看到,利用闭包可以在函数外部控制函数内部的值。所以,利用闭包的核心是将函数和环境保存到外部变量。
我们都知道,当函数执行完毕,执行上下文离开作用域(这里指函数作用域),作用域会被立即销毁,按照常理来说代码不可能运行起来。但是因为返回了一个闭包,这之后闭包为countUp
函数提供环境,这就是为什么上面的例子中立即执行函数执行完之后,通过countUp
访问到的count
是之前的状态。
所以,在使用闭包的时候你需要清楚的一点是,闭包会造成内存泄漏,如果你返回了一个函数作为闭包,只要返回的函数存在,这个闭包就一直存在,会占用内存。
说点其他的
这里我们利用《Understanding ECMAScript 6》块级绑定中的一个例子
典型错误
1 | var funcs = []; |
在第 3 行,console.log()
使用了变量i
,它的闭包中除了本身,还应该保存有变量i
,这个闭包被保存到了funcs[i]
,但你要注意到,变量i
是用var
声明的,它在函数作用域中,i
存在的作用域只有 window 一个,在循环中虽然产生了 10 个闭包,但他们都保存着同一个i
,当循环执行完,i = 10
,所以最后从闭包中取到的i
也全都是 10。
解决思路
上面提到了,因为变量i
存在的作用域只有一个,而一个作用域中只能有一个i
,要使i
值不一样,就需要创建不同的作用域,解决方法有两个。
方法一:使用块级作用域
只需要把var
声明改为let
声明即可
1 | var funcs = []; |
在 for 循环初始化的时候使用进行 let 声明,这时变量的作用域是局部块作用域,每进入一次循环都会重新创建一个i
变量。所以每次循环产生的闭包中的i
都来自不同的作用域,而且每个作用域中的i
都是随循环变化的,最终达成目的输出 0~9。
这时候不妨回去检验一下(已经头晕请跳过)
我们将i
变量和funcs
变量放在同级作用域下,代码如下:
1 | var funcs = []; |
我不是故意把你弄晕的,这样就能再次证明导致 bug 的根本原因是 10 个闭包使用同一个作用域的变量。
方法二:创建函数作用域
这种方法的思路是创建函数内部的i
变量,根据作用域链规则,当函数内部存在i
变量时,就会屏蔽掉上层的i
变量,通过函数返回闭包,这样闭包内的i
变量就来自于函数,而不是函数上层的i
变量。每个函数都会生成独一无二的作用域,根据上面的结论,同样能解决问题。
1 | var funcs = []; |