前端核心知识点3:闭包(2)


闭包并不是 JavaScript 特有的概念,社区上对于闭包的定义也并不完全相同。虽然本质上表达的意思相似,但是晦涩且多样的定义仍然给初学者带来了困惑。我自己认为比较容易理解的闭包定义为:
函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。
function numGenerator() { let num = 1; num++; return () => { console.log(num); };}
var getNum = numGenerator();getNum();
这个简单的闭包例子中,numGenerator 创建了一个变量 num,返回打印 num 值的匿名函数,这个函数引用了变量 num,使得外部可以通过调用 getNum 方法访问到变量 num,因此在 numGenerator 执行完毕后,即相关调用栈出栈后,变量 num 不会消失,仍然有机会被外界访问。



内存管理

内存管理是计算机科学中的概念。不论是什么程序语言,内存管理都是指对内存生命周期的管理,而内存的生命周期无外乎:
  • 分配内存空间
  • 读写内存
  • 释放内存空间
我们用代码来举例:
var foo = 'bar' // 在堆内存中给变量分配空间alert(foo) // 使用内存foo = null // 释放内存空间

内存管理基本概念
我们知道内存空间可以分为栈空间和堆空间,其中
  • 栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。
  • 堆空间:一般由开发者分配释放,这部分空间就要考虑垃圾回收的问题。
在 JavaScript 中,数据类型包括(未包含 ES Next 新数据类型):
  • 基本数据类型,如 Undefined、Null、Number、Boolean、String 等
  • 引用类型,如 Object、Array、Function 等
一般情况下,基本数据类型保存在栈内存当中,引用类型保存在堆内存当中。如下代码:
var a = 11var b = 10var c = [1, 2, 3]var d = { e: 20 }

对于分配内存和读写内存的行为所有语言都较为一致,但释放内存空间在不同语言之间有差异。例如,JavaScript 依赖宿主浏览器的垃圾回收机制,一般情况下不用程序员操心。但这并不表示万事大吉,某些情况下依然会出现内存泄漏现象。

内存泄漏是指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。这是一个非常「玄学」的概念,因为内存空间是否还在使用,某种程度上是不可判定问题,或者判定成本很高。内存泄漏危害却非常直观:它会直接导致程序运行缓慢,甚至崩溃。

内存泄漏场景举例
我们来看几个典型引起内存泄漏的例子:
var element = document.getElementById("element")element.mark = "marked"// 移除 element 节点function remove() { element.parentNode.removeChild(element)}
上面的代码,我们只是把 id 为 element 的节点移除,但是变量 element 依然存在,该节点占有的内存无法被释放。
请仔细参考下图:
我们需要在 remove 方法中添加:element = null,这样更为稳妥。
再来看个示例:
var element = document.getElementById('element')element.innerHTML = '点击'
var button = document.getElementById('button')button.addEventListener('click', function() { // ...})
element.innerHTML = ''
这段代码执行后,因为 element.innerHTML = '',button 元素已经从 DOM 中移除了,但是由于其事件处理句柄还在,所以依然无法被垃圾回收。我们还需要增加 removeEventListener,防止内存泄漏。
另一个示例:
function foo() { var name = 'lucas' window.setInterval(function() { console.log(name) }, 1000)}
foo()

浏览器垃圾回收
当然,除了开发者主动保证以外,大部分的场景浏览器都会依靠:
  • 标记清除
  • 引用计数

内存泄漏和垃圾回收注意事项
关于内存泄漏和垃圾回收,要在实战中分析,不能完全停留在理论层面,毕竟如今浏览器千变万化且一直在演进当中。从以上示例我们可以看出,借助闭包来绑定数据变量,可以保护这些数据变量的内存块在闭包存活时,始终不被垃圾回收机制回收。因此,闭包使用不当,极可能引发内存泄漏,需要格外注意。以下代码:
function foo() { let value = 123 function bar() { alert(value) } return bar}
let bar = foo()
这种情况下,变量 value 将会保存在内存中,如果加上:
bar = null

这样的话,随着 bar 不再被引用,value 也会被清除。
结合浏览器引擎的优化情况,我们对上述代码进行改动:
function foo() { let value = Math.random() function bar() { debugger } return bar}
let bar = foo()bar()
在 Chrome 浏览器 V8 最新引擎中,执行上述代码。我们在函数 bar 中打断点,会发现 value 没有被引用,如下图:
而我们在 bar 函数中加入对 value 的引用:
function foo() { let value = Math.random() function bar() { console.log(value) debugger } return bar}
let bar = foo()bar()
会发现此时引擎中存在闭包变量 value 值。如下图: