让人疑惑的闭包

让人迷惑的闭包

闭包是 JavaScript 中一个非常容易让人迷惑的知识点:

在 《你不知道的 JavaScript(上卷)》中有一段作者关于闭包的启示:

不知道的JavaScript.png

闭包确实是 JavaScript 中一个很难理解的知识点,接下来我们就对其一步步来进行剖析,看看它到底有什么神奇之
处。

函数是一等公民

在 JavaScript 中,函数是非常重要的,并且是一等公民:

  • 那么就意味着函数的使用是非常灵活的;
  • 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;

你可以使用内置的高阶函数,甚至可以自己编写高阶函数

闭包的定义

这里先来看一下闭包的定义,分成两个:在计算机科学中 和 在 JavaScript 中。

在计算机科学中对闭包的定义(维基百科):

  • 闭包(英语:Closure),又称 词法闭包(Lexical Closure)或 函数闭包(function closures)
  • 是在支持 头等函数 的编程语言中,实现词法绑定的一种技术
  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)
  • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行

闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JavaScript 中有闭包

因为 JavaScript 中有大量的设计是来源于 Scheme 的

我们再来看一下 MDN 对 JavaScript 闭包的解释:MDN 闭包

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
  • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
  • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来

那么我的理解和总结:

  • 一个普通的函数 function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
  • 从广义的角度来说:JavaScript 中的函数都是闭包
  • 从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

光看定义可能会很懵,后面我会通过内存的角度来解析闭包

闭包的访问过程

1
2
3
4
5
6
7
8
9
1 function foo() {
2 var username = 'wpf';
3 function bar() {
4 console.log(username);
5 }
6 return bar;
7 }
8 var fn = foo();
9 fn();

例如上述代码, 函数 bar 和它外部的自由变量 username ,这俩部分组合起来就是一个闭包。

从内存角度分析:

  1. 此时 GO(global object) 中 变量 fn 目前为 undefined
  2. 当代码执行到 第 8 行后, foo 函数被调用了,之后就会被销毁, 然后 变量 fn 指向 bar 函数
  3. 当执行第 9 行时,bar 函数被调用, 但之后不会被销毁,因为 bar 函数还引用这一个外部变量 username
  4. 此时导致闭包的形成

内存泄漏

我们经常听说 闭包是有内存泄漏的

  • 在上面的案例中, 如果 bar 函数 只用了一次,后续如果我们不再使用 bar 函数了,那么该函数对象应该要被销毁掉
  • 但是目前因为在全局作用域下 fn 变量 对 bar 函数有引用,而 bar 函数还引用这外部变量,导致内存无法被释放
  • 因为后续我们不会用到它了,久而久之就可能造成内存泄漏

如何解决内存泄漏这个问题呢?

很简单,将 fn = null ,fn 变量引用 bar 函数的 ”链“ 就会被断开, 这样根据 垃圾回收算法,就会把 bar 函数回收掉,即销毁掉。

AO 不使用的属性

我们再来研究一个问题: 当 AO 对象不会被销毁时,里面的所有属性是否都不会被释放?

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var username = 'wpf'
var age = 18

return function bar() {
debugger
console.log(username)
}
}

var fn = foo()
fn()

上面这段代码中 变量 age 属于闭包父作用域里面的变量, 我们知道形式闭包之后, 变量 username 一定不会被销毁 那么 变量 age 是否会被销毁呢?

我打了个断点,可以在浏览器上看下结果:

闭包.png

发现 变量 age 是被销毁了的