重学闭包
前言
当我们在讨论JavaScript的概念时,我们有两种视角来理解它们:静态视角与动态视角(或编译视角和运行视角)。
让我们以一个概念来举例:作用域 。
请看以下代码:
function func() { // A
const a = 1
} // B
func()
console.log(a)
如果以静态视角来理解,从A到B的区间属于一个词法作用域,所谓词法作用域(也称静态作用域),指的是:未来运行时,引擎按照我们代码所框定的区域进行变量的绑定 。换句话说,仅仅通过分析代码,就可以知道,console.log(a)
无法获得正确的值,因为此处的 a
明显在声明变量 a
的区块外。
因此, 静态视角理解概念,无需在大脑中模拟一遍执行效果,就可以分析出结论。
当然,我们也可以通过动态视角来理解它。
在这个例子中, func
函数运行时,会初始化一个 Function Environment Record(函数环境记录) 对象,这个对象用来描述当前函数的词法环境。当函数执行结束时,执行上下文被销毁,同样,函数环境记录也会销毁。此时,通过 console.log(a)
所在的全局环境记录对象中,无法找到变量 a
的绑定,因此代码执行报错。
动态视角要求我们在大脑中执行代码,并按照运行时的状态来理解概念。
闭包的静态视角
function createIncFunc() {
let a = 1
return function inc() {
console.log(a++)
}
}
const inc = createIncFunc()
inc() // 1
inc() // 2
inc() // 3
在这个例子中,我们在 createIncFunc
函数的作用域内,声明并返回了另一个函数 inc
,因此我们断定 inc
函数会产生闭包,它可以访问上层作用域中变量 a
的值。
因此,如果从静态视角的概念理解闭包,我们可以简单归纳闭包的概念是:
当一个函数定义在另一个函数的作用域内会产生闭包,这个内部函数可以引用它的上层作用域中的变量。
或者简化为:
只要函数嵌套函数,就会产生闭包。
很遗憾的是,我只能说,这种静态视角对闭包的理解是肤浅的,是不得精髓的,仅仅理解到这个层面,只是解决了什么是闭包的问题,但没有解决为什么是闭包以及闭包如何运转的问题 ,要想更深入理解闭包,我们必须请动态视角出山了。
闭包的动态视角
在开启动态视角讲解前,请读者先阅读 执行上下文 章节,并确保对以下概念有所理解:
- 执行栈(调用栈)
- 词法环境与环境记录(Environment Record)
充分理解这两个概念就可以对闭包的原理进行彻底的解释了。让我们开始吧!
function createIncFunc() {
let a = 1
return function inc() {
console.log(a++)
}
}
const inc = createIncFunc()
inc() // 1
inc() // 2
inc() // 3
还是这个例子,让我们在大脑中模拟JavaScript引擎的工作:
- 构建全局环境,准备执行代码;
- 创建 全局执行上下文 ,调用栈开始执行第一行代码;
- 代码执行至
const inc = createIncFunc()
,createIncFunc
开始执行; - 创建 函数执行上下文 (以下简称函数执行上下文A) ,该过程会创建一个函数环境记录(以下简称函数环境记录A)、关联Realm等,函数执行被推入执行栈,执行上下文A作为运行执行上下文(running execution context);
- 代码执行到
let a = 1
,函数环境记录绑定变量a
; - 代码执行到
return
语句处,函数环境记录绑定函数inc
; createIncFunc
函数执行完毕,函数执行被弹出执行栈,由于返回值被inc
变量绑定,因此执行上下文A没有被销毁,而是被保持了! ;- 代码执行到第一个
inc()
,创建函数执行上下文(以下简称执行上下文B),入执行栈,执行上下文B称为新的运行执行上下文; - 代码执行到
console.log(a++)
,尝试在当前函数环境记录中查找变量a
,没有找到; - 继续在当前函数环境记录的
[[OuterEnv]]
中查找,找到了函数环境记录A中绑定的变量a
,打印a
并对其做自增操作,输出1
; - 代码执行到第二个
inc()
,重复步骤8-10,输出2
; - 代码执行到第三个
inc()
,重复步骤8-10,输出3
; - 所有代码执行完毕,函数执行上下文A销毁,闭包结束。
这个过程充分解释了闭包的产生、作用与销毁过程,因为内部函数被引用,导致外部函数的执行上下文没有销毁,当未来内部函数调用时,仍然可以获取到上层函数执行上下文中的数据,这就是闭包。
闭包的用途
闭包无处不在! ,闭包是典型的 不需要了解它,更不需要深入解释它,但是人人都会用它 的概念。考虑以下例子:
function clickSelf(el) {
el.addEventListener('click', function() {
console.log(el)
})
}
clickSelf(document.body)
这是一段非常简单的代码,当用户点击某个元素时在控制台打印它。el.addEventListener('click', function() {...})
这里的函数将会形成一个闭包,保证未来该函数执行时,仍然能获取到其上层函数作用域中的变量(在这个例子中是变量 el
)。
;(function() {
let count = 0
function Logger() {
}
Logger.prototype.log = function() {
console.log(this.el)
console.log(`Click ${++count} times.`)
}
Logger.prototype.bind = function(el) {
if (this.el) return
count = 0
this.el = el
this.el.addEventListener('click', () => {
this.log()
})
}
window.Logger = Logger
})()
const logger = new Logger()
logger.bind(document.body)
在这个例子中, Logger.prototype.log
函数也生成了一个闭包,它引用了上层匿名函数中的 count
变量。