让人疑惑的闭包
让人迷惑的闭包
闭包是 JavaScript 中一个非常容易让人迷惑的知识点:
在 《你不知道的 JavaScript(上卷)》中有一段作者关于闭包的启示:
闭包确实是 JavaScript 中一个很难理解的知识点,接下来我们就对其一步步来进行剖析,看看它到底有什么神奇之
处。
函数是一等公民
在 JavaScript 中,函数是非常重要的,并且是一等公民:
- 那么就意味着函数的使用是非常灵活的;
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;
你可以使用内置的高阶函数,甚至可以自己编写高阶函数
闭包的定义
这里先来看一下闭包的定义,分成两个:在计算机科学中 和 在 JavaScript 中。
在计算机科学中对闭包的定义(维基百科):
- 闭包(英语:Closure),又称 词法闭包(Lexical Closure)或 函数闭包(function closures)
- 是在支持 头等函数 的编程语言中,实现词法绑定的一种技术
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)
- 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行
闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JavaScript 中有闭包
因为 JavaScript 中有大量的设计是来源于 Scheme 的
我们再来看一下 MDN 对 JavaScript 闭包的解释:MDN 闭包
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
那么我的理解和总结:
- 一个普通的函数 function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
- 从广义的角度来说:JavaScript 中的函数都是闭包
- 从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包
光看定义可能会很懵,后面我会通过内存的角度来解析闭包
闭包的访问过程
1 |
|
例如上述代码, 函数 bar 和它外部的自由变量 username ,这俩部分组合起来就是一个闭包。
从内存角度分析:
- 此时 GO(global object) 中 变量 fn 目前为 undefined
- 当代码执行到 第 8 行后, foo 函数被调用了,之后就会被销毁, 然后 变量 fn 指向 bar 函数
- 当执行第 9 行时,bar 函数被调用, 但之后不会被销毁,因为 bar 函数还引用这一个外部变量 username
- 此时导致闭包的形成
内存泄漏
我们经常听说 闭包是有内存泄漏的
- 在上面的案例中, 如果 bar 函数 只用了一次,后续如果我们不再使用 bar 函数了,那么该函数对象应该要被销毁掉
- 但是目前因为在全局作用域下 fn 变量 对 bar 函数有引用,而 bar 函数还引用这外部变量,导致内存无法被释放
- 因为后续我们不会用到它了,久而久之就可能造成内存泄漏
如何解决内存泄漏这个问题呢?
很简单,将 fn = null
,fn 变量引用 bar 函数的 ”链“ 就会被断开, 这样根据 垃圾回收算法,就会把 bar 函数回收掉,即销毁掉。
AO 不使用的属性
我们再来研究一个问题: 当 AO 对象不会被销毁时,里面的所有属性是否都不会被释放?
1 |
|
上面这段代码中 变量 age 属于闭包父作用域里面的变量, 我们知道形式闭包之后, 变量 username 一定不会被销毁 那么 变量 age 是否会被销毁呢?
我打了个断点,可以在浏览器上看下结果:
发现 变量 age 是被销毁了的