关于闭包之我见

344 阅读4分钟

最近想跳槽,面试的时候聊到了闭包,被问的很狼狈,所以回到家仔细了学习了闭包相关的知识,发帖总结一下,希望大佬们斧正

执行环境

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的变量.
    但是这点似乎并不绝对.......