重学 JS | 聊聊闭包
正常情况下,定义一个函数后,会产生一个函数作用域,函数体内的局部变量只能在函数作用域中使用。当函数执行完成,函数所占的空间将会被回收,此时存在函数中的局部变量同样会被回收,便无法被访问到。倘若我们希望函数中的局部变量仍然可以被访问到,这时候就需要通过闭包了。
先看一个经典使用闭包的例子:
聊闭包之前,我们先看下执行上下文环境。
JS 中每段代码都会存在一个执行上下文环境中,而任何一个执行上下文都会存在于整体的执行上下文中。根据栈先进后出的特点,全局环境产生的执行上下文会最先入栈,存在于栈底。当新的函数调用时,变会产生新的执行上下文环境,压入栈中。当函数调用完成后,这个上下文环境及其中的数据都会被销毁,并弹出栈,从而进入之前的执行上下文环境中。
需要注意的是,处于活跃的执行上下文只能通过有一个。通过代码查看执行上下文的变化过程:
从进入第 1 行代码,进入全局执行上下文环境,此时执行上下文环境只存在全局执行上下文,推入栈底。
执行到第 9 行代码,调用 boo()函数,进入 boo()函数执行上下文,为当前活跃的执行上下文,加入栈中,
执行到第 7 行代码,调用 foo()函数,进入 fo0()函数执行上下文,为当前活跃的执行上下文,加入栈中,当 foo()执行完毕,则 foo 执行上下文出栈销毁。回到 bar()函数执行上下文,继续执行代码,执行完毕则出栈销毁。最后全局上下文执行完毕,栈被清空,流程执行结束。
倘若当代码执行完毕,执行上下文环境却无法干净的销毁,这就是我们要说的闭包。
官方对闭包有一个通用解释:
一个拥有许多变量和绑定了这些变量执行环境的表达式,通常是一个函数。
闭包有两个明显的特点:
函数拥有的外部变量的引用,在函数返回时,该变量仍处于活跃状态。
闭包作为一个函数返回时,其执行上下文不会被销毁,仍处于执行上下文中。
看个例子:
代码开始执行时,生成全局执行上下文环境,并压入栈底;
当执行到第 9 行代码时,调用 fn()函数,生成 fn()函数上下文环境,并压入栈中,返回 bn()函数,赋值给 f1。
当执行到第 10 行代码时,调用 f1()函数,因为 f1 函数中包含了对 max 的引用,而 max 变量是存在 fn 中的,因此 fn 函数执行上下文环境并不会被直接销毁,依然存在于执行上下文中。
当第 10 行代码执行结束,bn 函数执行上下文环境才会被销毁,同时 max 变量的引用会被释放,fn 函数的执行环境一同被销毁。
最后全局上下文环境执行完毕,栈被清空,流程执行结束。
从例子我们看出,闭包所存在的最大的问题就是消耗内存!
根据闭包的特点,我们可以利用闭包的特性来实现一些特性:
一、结果缓存
如果函数调用处理耗时,我们可以将结果在内存中缓存起来,下次执行时,若存在内存中,则直接返回,提升执行效率。
二、封装
模块化思想中是将具有一定特定的属性封装到一起,只需对外暴露对应的函数,并不关心内部实现。
小结
闭包使用合理,一定程度上能提高代码执行效率;如果使用不合理,则会造成内存浪费,性能下降。总结下闭包的优缺点。
优点:
包含函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
在适当的时候,可以在内存中维护变量并缓存,提高执行效率。
缺点:
消耗内存:通常来说,函数的活动对象会随着上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,因为闭包比一般函数消耗更多内存。
至此我们学习了闭包的特性、优缺点以及用法。
版权声明: 本文为 InfoQ 作者【梁龙先森】的原创文章。
原文链接:【http://xie.infoq.cn/article/df4082a252ee62a8c3940b67d】。文章转载请联系作者。
评论