前端对“闭包”的定义,网上的文章99%都会常熟在一个函数中新建一个函数,便是闭包。我认为这是结果,包括MDN上对闭包的定义也是改了又改,我们想要记住这个知识点,并且能灵活地运用到业务中,才能更好地去记忆。
说到闭包,那就得联系到作用域的概念。作用域主要分为全局作用域和局部作用域(这里不谈eval),想要在局部作用域中获取全局作用域的变量或方法是很方便的,而想要在全局作用域中获取局部作用域那就是。闭包的出现恰恰是解决局部数据共享的一个关键。
function A() {const a = 1function B() {console.log(a)}B()}
上述代码中,就有局部数据共享的情况发生,B函数调用了A函数的作用域下的a变量。但是变量a可能会造成内存泄漏,因为它不受浏览器垃圾回收机制的管控。但是,在很多场景下,我们就会运用到这个闭包的特性“局部数据共享”,比如在现代前端框架中,在一个父组件下引入两个子组件。
// 伪代码const Parent = () => {const [a, setA] = useState('a')return <div><ChildA a={a} /><ChildB a={a}/></div>}
上述代码中,父组件通过props将数据共享给两个子组件,这样同样形成了一个“局部数据共享”的情况,我认为这也是利用的闭包的一个特性。
在业务代码中,防抖与节流操作,同样也运用了闭包的特性。
我们先看防抖的例子:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><input type="text" id='input'><script>const debounce = (fn, delay = 0) => {// fn: 真正要执行的函数// delay: 防抖的延迟时间,在该时间内不再输入新的内容,就会执行 fn 函数let timer = null // 要用的的局部数据,这个数据在创建的时候就不能被销毁// debounce 方法需要返回一个新的方法,这就形成了一个闭包了。return () => {// 每次新的输入都要清空一下上一次赋值的 timer,目的是清理要执行的 fn 方法if (timer) {clearTimeout(timer)}// 这里需要延时处理 fn 方法timer = setTimeout(() => {fn()}, delay)}}const getData = () => {console.log('请求接口数据')}const input = document.getElementById('input')// 5秒内不输入内容的时候,就执行获取接口的方法input.oninput = debounce(getData, 5000)</script></body></html>
防抖函数debounce就是一个典型的闭包的应用场景,在防抖函数中return的函数中,使用了三个局部变量,分别是timer、fn、delay。
再说说节流,同样的,节流的使用场景也用到的闭包,节流的作用是,在设置的延时范围内,不再多次触发方法,直到方法执行完,才开始下一次方法的执行。示例代码如下:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Throttle</title></head><body><input type="text" id='input'><script>const throttle = (fn, delay) => {// 设置一个标示,它的作用是阻断后续的执行默认为 falselet flag = falsereturn () => {// 如果在执行的时候,直接 returnif (flag) {return}flag = truesetTimeout(() => {fn()flag = false}, delay)}}const getData = () => {console.log('请求接口数据')}const input = document.getElementById('input')// 5秒内不输入内容的时候,就执行获取接口的方法input.oninput = throttle(getData, 5000)</script></body></html>
上述节流方法,在5秒内只会执行一次请求数据方法。5秒后,flag作为局部变量,被设置为false,表示函数已经执行完成。
所以很多开发者都说要避免写出“闭包”的代码,函数内的局部变量不会被浏览器的垃圾回收机制及时清理。但是在某些应用场景下,闭包还是很香的,要善于使用这些特性给我们带来的能力,才能敲出更好的业务代码。
