闭包(Closure)作为前端面试经常被考察到的高频面试题,可谓是八股文常客。但答案常常参差不齐,简单一点说是函数中的函数,再深入一点提一下内存泄漏,还可以引申到作用域、垃圾回收、模块化等知识点。
闭包的定义
闭包(Closure)是所有函数式编程中都存在的概念,或叫做现象,并不是JS特有。不仅是JS,函数式编程中都存在闭包。
广义闭包
广义闭包指的是函数式编程闭包,闭包通用的定义是函数+外层作用域(词法作用域),也可以理解为任何函数都是闭包。
狭义闭包
狭义闭包指的是JS中的闭包。浏览器出于的性能考虑,对闭包的定义做了优化,当外部作用域有用到这个函数中的变量就会建立闭包,否则不建立。
所以,在ECMA标准中就加了一个前提条件,在一个函数环境中声明的函数才能称为闭包,那么,函数+外层作用域就是闭包的构成。【闭包MDN文档】
简单示例
function a() { var v1 = 1; function b() { return v1; } return b; } const c = a();
c不能直接访问b函数,但可以通过a函数间接访问b,拿到变量v1的值。意味着b函数不销毁,b函数的词法作用域也无法销毁。
应用场景
早期ES5的模块化
通过函数中定义函数的方式,实现私有化内部函数。
var exportModule = (function() { var a = 1; var b = 2; function f1() { return a; } function f2() { console.log(‘f2’); } });
高阶函数
将函数作为参数传递给另一个函数,做一些比较复杂的交互场景。
function a() { console.log(‘abc’); } function b(fn) { fn(); } b(a); // 输出:abc
回调函数
在早期没有Promise,没有async/await时,关于异步编程常用回调函数处理接口返回数据的操作。
function getData(cb) { setTimeout(function() { cb(‘data’); }, 1000); // 模拟请求数据 } getData(function(res) { console.log(res); // 输出:data });
优缺点
优势
- 保存变量:使变量留在内存中不被销毁,从而实现某种功能交互。
- 封装数据:函数包函数可以建立数据的私有作用域,使数据不暴露给全局,提高数据安全性。
劣势
- 复杂度提升:当出现函数套函数的情况,无论是代码复杂度还是逻辑复杂度都会上升,带来高昂的调试成本。
- 内存泄露:如果滥用闭包可能导致不可控的内存泄露,带来潜在风险,影响应用的性能。
- 心智负担:使用闭包时为了权衡功能和性能,需要考虑何时销毁,带来的负面影响等。
关于内存泄露
我们老说内存泄露,有几个变量占用内存就叫内存泄露吗?这也太夸张了吧!内存泄漏并不是怕多存几个变量或者多存几个函数,以现代的硬件配置不值一提。
其实最害怕的是关联词法作用域无法销毁导致的不可控扩大内存泄漏,以及穿插污染带来的深层次不易调试的逻辑性错误。内存泄露大致有这两种情况:
1、函数持有了本该被销毁的函数,造成了关联的词法作用域无法被销毁,导致内存泄露的扩大。
虽然一个函数以及几个变量占用的内存空间有限,但关联的词法作用域中拥有的变量较多,易造成不可控的影响。
const el = document.querySelector(‘.content-box’); const handleClick = () => { console.log(‘handleClick’); }; el.addEventListener(‘click’, handleClick); el.removeEventListener(‘click’, handleClick);
2、多函数共享词法作用域时,可能导致词法作用域留存无法销毁,从而出现词法作用域的某些变量无法访问也无法销毁的情况。
function a() { var v1 = ‘v1’; var v2 = ‘v2’; function f1() { return v1; } function f2() { return v2; } return f2; } const c = a();
这种情况下,可以通过a函数访问f2函数,间接访问到v2变量。但v1相当于闭塞在a函数内部,无法通过任何方式访问,v1本该被销毁,但实际还存在。
因为f1和f2共用了一个词法作用域,v2是有引用的,所以词法作用域不会被销毁,那么v1也存在了。
避免内存泄露
- 养成好习惯不滥用闭包,不要遇事不决就函数套函数。
- 当词法作用域中的变量不再使用,将其置null,便于垃圾回收标记识别。
- 监听器、timer等使用时不要忘记调用remove、clear等方法。
- 全局变量相当于在整个生命周期中滞留的内存变量,谨慎使用。