很多文章和资料说闭包是一个函数,根据 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”。
这些作用域构成的作用域链保证了当前执行环境对符合访问权限的变量的有序访问。
闭包和内存泄漏
每一个函数在编译时会生成一个 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
上面这段代码中
- 编译阶段:因为
foo1
使用了arr
,所以foo.Closure
引用了arr
; - 代码执行:
window.bar
持有了foo2
,foo2
的作用域链包含foo.Closure
,foo.Closure
引用了arr
,最终导致arr
无法释放产生内存泄漏。