前端对“闭包”的定义,网上的文章99%都会常熟在一个函数中新建一个函数,便是闭包。我认为这是结果,包括MDN上对闭包的定义也是改了又改,我们想要记住这个知识点,并且能灵活地运用到业务中,才能更好地去记忆。

    说到闭包,那就得联系到作用域的概念。作用域主要分为全局作用域和局部作用域(这里不谈eval),想要在局部作用域中获取全局作用域的变量或方法是很方便的,而想要在全局作用域中获取局部作用域那就是。闭包的出现恰恰是解决局部数据共享的一个关键。

    1. function A() {
    2. const a = 1
    3. function B() {
    4. console.log(a)
    5. }
    6. B()
    7. }

    上述代码中,就有局部数据共享的情况发生,B函数调用了A函数的作用域下的a变量。但是变量a可能会造成内存泄漏,因为它不受浏览器垃圾回收机制的管控。但是,在很多场景下,我们就会运用到这个闭包的特性“局部数据共享”,比如在现代前端框架中,在一个父组件下引入两个子组件。

    1. // 伪代码
    2. const Parent = () => {
    3. const [a, setA] = useState('a')
    4. return <div>
    5. <ChildA a={a} />
    6. <ChildB a={a}/>
    7. </div>
    8. }

    上述代码中,父组件通过props将数据共享给两个子组件,这样同样形成了一个“局部数据共享”的情况,我认为这也是利用的闭包的一个特性。

    在业务代码中,防抖与节流操作,同样也运用了闭包的特性。
    我们先看防抖的例子:

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Document</title>
    8. </head>
    9. <body>
    10. <input type="text" id='input'>
    11. <script>
    12. const debounce = (fn, delay = 0) => {
    13. // fn: 真正要执行的函数
    14. // delay: 防抖的延迟时间,在该时间内不再输入新的内容,就会执行 fn 函数
    15. let timer = null // 要用的的局部数据,这个数据在创建的时候就不能被销毁
    16. // debounce 方法需要返回一个新的方法,这就形成了一个闭包了。
    17. return () => {
    18. // 每次新的输入都要清空一下上一次赋值的 timer,目的是清理要执行的 fn 方法
    19. if (timer) {
    20. clearTimeout(timer)
    21. }
    22. // 这里需要延时处理 fn 方法
    23. timer = setTimeout(() => {
    24. fn()
    25. }, delay)
    26. }
    27. }
    28. const getData = () => {
    29. console.log('请求接口数据')
    30. }
    31. const input = document.getElementById('input')
    32. // 5秒内不输入内容的时候,就执行获取接口的方法
    33. input.oninput = debounce(getData, 5000)
    34. </script>
    35. </body>
    36. </html>

    防抖函数debounce就是一个典型的闭包的应用场景,在防抖函数中return的函数中,使用了三个局部变量,分别是timerfndelay

    再说说节流,同样的,节流的使用场景也用到的闭包,节流的作用是,在设置的延时范围内,不再多次触发方法,直到方法执行完,才开始下一次方法的执行。示例代码如下:

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Throttle</title>
    8. </head>
    9. <body>
    10. <input type="text" id='input'>
    11. <script>
    12. const throttle = (fn, delay) => {
    13. // 设置一个标示,它的作用是阻断后续的执行默认为 false
    14. let flag = false
    15. return () => {
    16. // 如果在执行的时候,直接 return
    17. if (flag) {
    18. return
    19. }
    20. flag = true
    21. setTimeout(() => {
    22. fn()
    23. flag = false
    24. }, delay)
    25. }
    26. }
    27. const getData = () => {
    28. console.log('请求接口数据')
    29. }
    30. const input = document.getElementById('input')
    31. // 5秒内不输入内容的时候,就执行获取接口的方法
    32. input.oninput = throttle(getData, 5000)
    33. </script>
    34. </body>
    35. </html>

    上述节流方法,在5秒内只会执行一次请求数据方法。5秒后,flag作为局部变量,被设置为false,表示函数已经执行完成。

    所以很多开发者都说要避免写出“闭包”的代码,函数内的局部变量不会被浏览器的垃圾回收机制及时清理。但是在某些应用场景下,闭包还是很香的,要善于使用这些特性给我们带来的能力,才能敲出更好的业务代码。