最近想跳槽,面试的时候聊到了闭包,被问的很狼狈,所以回到家仔细了学习了闭包相关的知识,发帖总结一下,希望大佬们斧正
执行环境
JS在运行每个函数时,都会为该函数创建执行环境(enecution context),在这个环境中,存在一个调用对象,所有在该函数能访问到的变量,都存在于该对象中,但是这个对象是解释器使用的,并不是开发者使用的.
作用域和作用域链
- 作用域
需要明确指出的是,作用域是针对变量的,是解释器寻找变量值时所遵循的一种规则.- 在JS中, 全局变量有全局作用域,函数内声明的变量有局部作用域(暂且不谈let,const).
- 函数(局部)作用域是在函数定义的时候就已经确定的,而不是在调用时确定的,这一点和执行环境不同.
- 作用域链
当多个函数嵌套时,就会引出作用域链的概念,可以认为单独的作用域就是一个对象,嵌套时作用域是一个数组.访问变量时,离变量最近的这个函数是数组的第一位,上一层函数是第二位,全局作用域是最后一位
前面说过,作用域是函数定义的时候就已经确定的,但是作用域链并不是死的,是可以改变的,这就是所谓的闭包
闭包现象
正常情况下执行函数的顺序应该是这样的:
- 函数执行,依据作用域链确定相关的值,创建执行环境和其中的调用对象
- 开始计算
- 计算结束,销毁当前执行环境和调用对象,等待垃圾回收机制将函数中用过且没有后续引用的变量回收. 但是有例外:
function fn(){
var a = 10
function bar(){
console.log(a)
}
bar()
}
fn()
现在出现了嵌套函数,fn函数的执行环境与调用对象并不能直接被销毁,因为他其中还有bar函数需要执行,这样也没问题,等bar执行之后再销毁也不迟.
但是如果bar函数被当成返回值返回时,fn函数执行环境和调用对象就不能正常的被销毁了:
function fn(){
var a = 10;
function bar(){
console.log(a)
}
return bar
}
var bar = fn()
bar();
这时候fn的执行环境已经被销毁了,但是调用对象却还在内存中维持着,因为外部存在一个bar在引用fn的调用对象中的值.bar可能在任意地方被调用.外部可以通过bar来访问到fn内部的变量了,换句话说,fn的作用域链被延长了. 这种现象我们称之为闭包.
闭包定义
其实真正的闭包随处可见,只要有函数,并且函数内使用了上级作用域的变量,就已经形成了闭包.
但是只有当函数内的函数被返回,暴露给了全局,导致函数的作用域链被延长时.这种闭包才有趣. 本片文章所说的闭包,也都是这种闭包
闭包的优点
- 闭包可以使函数的作用域延长,以至于可以在外部访问到函数内部的变量.
- 闭包可以维持一个变量一直在内存中,不会被垃圾回收机制回收.
闭包的缺点
- 成也萧何败萧何,闭包导致了一个调用对象长期驻扎在内存中无法被自动回收,也就是所谓的内存泄漏.
所以我们使用闭包之后,要记得清除这个闭包
bar = null
把引用的变量标记为null就清除了这个引用,下次垃圾回收机制运行时,就可以正常的带走该调用对象了.
遗留的思考
- 回调函数,到底算不算闭包?
回调函数是指执行一个函数后,调用另一个函数,并且允许传入参数,按照上面定义来说,应该不算是闭包,因为并没有形成一直驻扎在内存中并且需要手动赋值为null的变量.
但是这点似乎并不绝对.......