闭包在JavaScript中是一个很难理解的知识点,接下来我们会一步一步进行剖析。
闭包的定义
计算机科学
在计算机科学中,闭包(Closure) 也叫做词法闭包(Lexical Closure) 或者函数闭包(function closures);是在支持头等函数的编程语言中,实现词法绑定的一种技术。
在实现上,闭包是一个结构体,存储了一个函数和一个关联的环境;闭包跟函数的区别在于,捕捉闭包的时候,自由变量会在捕捉时被确定,即使脱离了捕捉时的上下文,也能照常运行。
什么叫头等函数?
头等函数是指在程序设计语言中,函数被当作一等公民。
这也就是说,函数的使用是非常灵活的,它可以作为另一个函数的参数,也可以作为另一个函数的返回值来使用,同时它也可以被赋值给一个变量、对象属性或者是数组元素。(偶尔会有面试问这个)
这就不得不说到高阶函数了。
高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者是返回它们。 简单来说,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。
像我们平时数组的map方法、filter方法等等,其实都是高阶函数。
[1,2,3,4].map(item=>item) // map方法就是一个高阶函数,因为接受了一个函数作为参数
JavaScript中的闭包
而在JavaScript中,闭包就是一个函数和对其周围状态(词法环境,lexical environment)的引用捆绑在一起的组合。
换而言之,闭包就是让我们可以在一个函数的内部去访问到外层函数的作用域。在JS中,我们创建一个函数的时候,闭包就会在函数创建的同时被创建出来。
所以就有了2种说法:
- JavaScript中,函数都是闭包。
- JavaScript中,如果一个函数访问了外层作用域的变量,那就是闭包。
但是上述的几种说法,并不妨碍我们的理解,闭包就是让我们可以在一个函数的内部去访问到外层函数的作用域。
闭包的访问过程
假如我们有以下代码:
function fatherFun(num) {
return function Son(num2) {
return num + num2
}
}
var resultFun = fatherFun(1)
console.log(resultFun(2)) // 3
如图所示,在我们编写代码的时候,就已经形成了闭包。fatherFun函数和全局对象的引用捆绑在了一起,在函数内部能够访问到全局对象的变量。Son函数则和fatherFun函数的作用域绑定在了一起,能够在Son函数内部访问到father函数的创建的变量,如num。
当函数继续执行
如图所示,Father函数已经执行完毕出栈;正常情况下Father函数产生的AO对象会被释放,但是在Son函数中,有作用域指针指向了这个AO对象,所以就不会被释放。
补充——内存泄漏
为什么会有内存泄漏呢?
像上述代码所示,我们fatherFun函数对应的作用域AO应该在函数被销毁时,也需要被销毁;但是由于Son函数中存在对0x200的引用,所以会造成内存无法被释放,垃圾回收不会去清理它。
但是,当我们有大量的内存没有被释放时,就会容易造成页面的卡顿。
如何解决?
闭包中出现内存泄漏是不可避免的,重要的是我们要了解闭包是何时创建的以及它保留了哪些对象,了解闭包的预期生命周期和用途(尤其是作为回调使用时)。
当我们在上述代码中,将resultFun设置为null之后,那么在垃圾回收机制的下一次检测中,就会对那些变量进行销毁。