深入理解JavaScript系列(7):闭包
介绍
闭包,在外面介绍了前面的之后,就可以了解我们的闭包了。
概念
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
我们先来简单回顾一下执行上下文的第二个例子吧。
1 | var name = 'hzq'; |
我们这上面这段代码来看一下执行上下文栈和执行上下文的情况。
- 进入全局代码创建全局上下文,
globalContext
入栈 - 全局代码执行
- 执行
checkName
函数,创建checkName
函数执行上下文,checkNameContext
入栈 checkName
执行上下文初始化,创建变量对象、作用域链、this
等checkName
函数执行完毕,出栈- 执行
fn
函数,创建fn
函数执行上下文,fn
函数上下文入栈 fn
执行上下文初始化,创建变量对象、作用域链、this
等fn
函数执行完毕,fn
函数上下文弹出- 执行其他全局代码,代码执行完毕,全局上下文弹出
我们看这个过程时,发现 checkName
函数已经不在执行上下文栈中,为什么还可以取到 name
的值呢?
从 《深入理解 JavaScript 系列(4):作用域链》 中,我们了解到,fn
函数,其实维护了一个作用域链,可以看看。
1 | fnContext = { |
所有,其实 fn1
取到的就是上层的函数。就是因为作用域链,实现了父级函数即使被销毁,也可以取到值。
我们先举一个简单的闭包使用方式。
1 | function fn() { |
我们的执行过程类似,但是在作用域链中保存着值,可以进行修改,也导致了一个问题,也就是内存删除不干净,内存泄露。
必刷面试题
我们来看一道经典的闭包面试题。
1 | const data = []; |
这题是我们经常遇到的题目,为什么打印的全是 5
呢?
我们来看一下全局上下文中,执行函数之前的 globalContext
,如果对此有问题,可以去看一下前面的几篇文章。《深入理解 JavaScript 系列(2):执行上下文栈》、《深入理解 JavaScript 系列(3):变量对象》、《深入理解 JavaScript 系列(4):作用域链》、《深入理解 JavaScript 系列(5):this》。
我们来看一下上面代码的全局上下文(globalContext
) 的内容是什么。
1 | globalContext = { |
这样就一定都打印 5
了吗? 不是的,我们还需要看看 data
中函数的上下文。
1 | data[0]Context = { |
所以,我们在 data[0]
函数中没有找到结果 i
,于是从 global
中寻找,于是找到结果为 5
。
1 | const data = []; |
这变成了一个立即执行函数。
我们在全局上下文的 VO 是没有变化的。
但是我们来看看 data[0]
函数执行时,其中有一个是匿名函数,我们假设为 NoName
函数,我们来看一下 data[0]
的上下文
1 | data[0]Context = { |
在来看看匿名函数的上下文
1 | NoNameContext = { |
所以,这时候执行时,找到了 i = 0
,返回结果。