闭包是什么?为什么闭包会造成内存泄漏?

425 阅读2分钟

很多文章和资料说闭包是一个函数,根据 MDN 文档的描述和我的理解,更准确的说,闭包是一个组合,由函数及其声明时的词法环境组成,即函数绑定了一些外部状态的引用。

这让我们可以在函数内部访问到外部环境变量,即便外部执行上下文已经被销毁了。比如我们使用 debounce 返回一个函数,这个函数就引用了一个闭包变量,每次执行这个函数,都能访问到这个闭包变量。

之所以会说闭包是函数,是因为在 JS 中闭包不需要特殊声明,每个函数都能通过作用域链访问到外部环境变量。

闭包作用域链 Closure scope chain

作用域链的形成是基于 JavaScript 采用的词法作用域(Lexical Scope)机制,JavaScript 引擎在编译阶段就确定了变量和函数的作用域。

作用域是当前执行上下文,其中的值和表达式是“可见”的或可以被引用的。JS有以下几种作用域:

  • Block scope: The scope created with a pair of curly braces
  • Function scope: The scope created with a function.
    • Local scope
    • Closure scope
  • Module scope: The scope for code running in module mode.
  • Global scope: The default scope for all code running in script mode.

debugger 会将 script 顶层声明的变量显示为 “Script scope”,这只是为了帮助区分全局变量,本质上还是属于“Global scope”。

这些作用域构成的作用域链保证了当前执行环境对符合访问权限的变量的有序访问。

image.png

闭包和内存泄漏

每一个函数在编译时会生成一个 Closure 对象,分析函数中声明的内部函数使用了函数词法环境中的哪些变量,然后将这些变量的引用加入 Closure 对象,最终这个闭包对象将作为这些内部函数[[Scopes]]属性(即作用域链)中的一员。这样即便函数被销毁,内部函数还是可以通过 Closure 对象访问到外部函数声明的变量。

闭包产生内存泄漏的根本原因是因为 Closure被其所有内部函数作用域链引用,只要有一个内部函数没有销毁,Closure 就无法销毁,导致其引用的变量也无法销毁,最终产生了内存泄漏。

function foo(){
    let arr = Array(10000000)
    function foo1(){
        console.log(arr)
    }
    return function foo2(){}
}
window.bar = foo()
// window.bar --> foo2 --> foo.Closure --> arr

上面这段代码中

  1. 编译阶段:因为 foo1 使用了 arr,所以 foo.Closure 引用了 arr
  2. 代码执行:window.bar持有了foo2foo2 的作用域链包含 foo.Closurefoo.Closure 引用了 arr,最终导致 arr 无法释放产生内存泄漏。

相关资料: