• JS 中防抖函数形成闭包的相关处理及思考


    最近很长一段时间因为各种琐事,已经很久没有更新博客了,其实一直在工作中,但是一方面是没时间更博客,另一方面也是因为没有什么特别想写的点,最近看了下这篇文章在 JS 文件和 Vue 组件中使用防抖节流函数_流光D的博客-CSDN博客还是有不少流量的,评论有人提问

    我想知道这个形成的闭包怎么清除掉,直接给这个闭包赋值为null吗

    刚好提示了我可以写一篇关于理解 JS 闭包的文章,笔者将会会在近期更新出来。

    回到上述问题,我们说 JS 中的防抖节流工具函数的实现原理是一般是利用 JS 闭包特性来实现的,当然也可以不用闭包,如下例子就没有使用闭包实现了简单的防抖:

    1. // vue组件中
    2. data () {
    3. return {
    4. timer: null
    5. }
    6. },
    7. methods: {
    8. getXX () {
    9. if (this.timer) {
    10. clearTimeout(this.timer)
    11. }
    12. this.timer = setTimeout(() => {
    13. // 具体的业务代码
    14. }, 500) // 假设防抖间隔是500ms
    15. }
    16. }

    当然,这是一个 vue 文件。JS 闭包在代码中起到的作用,简单的说主要有两点,一是保护;二是保存。详见笔者之后更新的文章。而在本文中笔者介绍到的防抖工具函数的实现:

    1. function debounce(func, wait, immediate) {
    2. var timeout
    3. return function () {
    4. var context = this;
    5. var args = arguments;
    6. if (timeout) {
    7. clearTimeout(timeout);
    8. }
    9. if (immediate) {
    10. // 如果已经执行过,不再执行
    11. var callNow = !timeout;
    12. timeout = setTimeout(function(){
    13. timeout = null;
    14. func.apply(context, args) // 这行代码可不加,详细说明见下文
    15. }, wait)
    16. if (callNow) func.apply(context, args)
    17. }
    18. else {
    19. timeout = setTimeout(function(){
    20. func.apply(context, args)
    21. }, wait);
    22. }
    23. }
    24. }

     debounce 函数中定义了一个名为 timeout 的局部变量,该函数接受三个参数,分布是 func (作为需要被防抖处理的函数),wait(防抖时间),immediate(执行防抖处理时,是否先立即触发一次被防抖处理的函数)。debounce 函数返回了一个匿名函数(以下简称匿名函数 A):

     在返回的匿名函数 A 中,使用了其父作用域的变量 timeout, 由此,本该在 debounce 函数执行完后被 JS 垃圾自动回收系统回收的 timeout 没有被回收,而是被保存在了内存中。于是形成了我们常说的闭包:

    一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

    在匿名函数 A 中,timeout 被用来作为 setTimeout 函数的返回值,因为防抖函数要实现的效果是在高频次尝试触发某个行为时,阻止该行为发生,而在一段时间(wait)后都没有尝试触发该行为时,再执行该行为(生活中这样的例子很常见,比如公交车司机停车后等待乘客全部上车后关门),因而我们需要 timeout 变量保存之前的 id 值(有 id 值存在则说明在此之前有尝试触发过该行为)。所以我们在实现上既可以通过在全局作用域声明 timeout, 也可以通过形成闭包来保存 timeout 的值。从函数封装的角度来说,通过闭包来实现防抖更为优雅。

    回到最开始的问题,debounce 函数形成的闭包如何清除?

    答:并不需要清除 debounce 函数所形成的闭包,因为 debounce 函数被使用的本意便是利用其形成的闭包保存 timeout 这个变量,通过 timeout 变量的值来进行逻辑判断。这里还有一个问题:如果非要回收 debounce 函数中的 timeout 变量呢?比如说当我们明确在 debounce 函数已经被调用了之后,我们心里不舒服,觉得 timeout 没有被系统回收很不爽,那我们可以回收 timeout 变量吗?当然可以,为了便于理解我们写一个简单的闭包例子

    1. function a(){
    2. var b = 0;
    3. setTimeout(() => {
    4. // alert(b)
    5. debugger
    6. }, 3000)
    7. return function(){
    8. b++;
    9. console.log(b);
    10. }
    11. }
    12. var d = a();
    13. d();//1
    14. d();//2
    15. d();// 3
    16. d = null // ? 可以回收 变量b 吗

    如上所述,如果把 d 赋值为 null, 变量 b 会被回收吗,我们用 setTimeout 加 debugger 的方法测试:

    发现 变量b 并没有被回收,仍然是 3. 

    因而通过将 d 赋值为 null 的方式不能回收变量b, 于是笔者想到另一种方法,如下:

    1. function a(){
    2. var b = 0;
    3. setTimeout(() => {
    4. // alert(b)
    5. debugger
    6. }, 3000)
    7. return function(flag){
    8. if (flag) {
    9. b = null
    10. return
    11. }
    12. b++;
    13. console.log(b);
    14. }
    15. }
    16. var d = a();
    17. d();//1
    18. d();//2
    19. d();// 3
    20. d(true)

     在返回的闭包中设置一个 flag 变量值,手动清除变量b 的值。

    通过 debugger 我们发现变量b 变成了 null, 必然会被系统回收。当然这样的用法很少见,因为一般来说,只要函数d 不是全局函数,在其对应的父作用域被销毁后函数d 的子作用域也必然销毁,变量b 即使不再使用,也无需刻意手动赋值触发自动 GC。不过,如果函数d 是一个全局变量,那么有必要注意形成闭包后是否会导致其一直常驻内存,引起不必要的内存资源浪费。 

    总结:

    本文对于 JS 中闭包的出现原因及作用描述还比较浅显,关于闭包形成后保存的变量是否应该回收,如何回收这一块的理解也未十分全面,之后还会更新一篇关于 JS 闭包的文章,对 JS 闭包作更全面深入的讨论 。

    参考:

    https://www.jianshu.com/p/00c747510df5

  • 相关阅读:
    为什么Redis默认序列化器处理之后的key会带有乱码?
    Asp.Net Core 中使用配置文件
    转载:丰子恺迭浪式阅读法
    如何化解35岁危机?华为云数据库首席架构师20年技术经验分享
    【c语言】--qsort快速排序【附模拟实现】
    蓝桥杯-平方和(599)
    需要影视解说配音的看过来,用它就对了
    小团队之间有哪些好用免费的多人协同办公软件
    深度学习系列50:苹果m1芯片加速pytorch
    在pandas中使matplotlib动态画子图的两种方法【推荐gridspec】
  • 原文地址:https://blog.csdn.net/a715167986/article/details/125768479