设计模式实战:用模块模式写一个防抖函数以理解闭包

650 阅读4分钟

相信很多新人学习js的时候都绕不开一个问题,闭包。

一句话说清楚,闭包是由函数以及声明该函数的词法环境组合而成的,用不准确的大白话来说,就是函数(对象)中的函数(js中万物皆对象,函数是一种特殊的对象)。

那么问题来了,这玩意有什么用,函数中的函数不到处都是,为什么搞得这么神秘,面试中老喜欢问。下面就由我黒网吧天才少年带大家(自己)分析一段花裤衩老师在vue-element-admin中的一段防抖工具函数。

请看题:

/**
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} immediate
 * @return {*}
 */
export function debounce(func, wait, immediate) {
  let timeout, args, context, timestamp, result

  const later = function() {
    // 据上一次触发时间间隔
    const last = +new Date() - timestamp

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last)
    } else {
      timeout = null
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args)
        if (!timeout) context = args = null
      }
    }
  }

  return function(...args) {
    context = this
    timestamp = +new Date()
    const callNow = immediate && !timeout
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait)
    if (callNow) {
      result = func.apply(context, args)
      context = args = null
    }

    return result
  }
}

一眼看下去,这是高手。

标准的模块模式设计模式。一种对闭包应用最为清晰的设计模式。(注:模块模式是为单例模式添加私有变量和私有方法,并减少全局变量的使用)

用闭包模拟私有方法

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:increment,``decrementvalue

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个函数外部直接访问。必须通过函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

请注意两个计数器 Counter1Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

接下来我们就可以很清晰地分析一下在调用这个工具函数的时候,它发生了什么:

import { debounce } from "@/utils/index";
const func = ()=>{ console.log(1) }
const btn1Obj = document.getElementById("btn2");
btn1Obj.addEventListener("click",debounce(func,3000),false);

1、import 并且在如果你调用过debounce的话,js会开辟了一块空间(调用几次开辟几个,互不干扰,能看出来使用闭包的缺点吗),创建了局部变量timeout, args, context, timestamp, result,还有一个later的函数,最后返回一个匿名函数,因为这个匿名函数内部有引用debounce中创建的变量,相互关连,支撑住了这块词法环境没有被垃圾回收,而是一直保留。

2、初始化完成之后,当你触发这个函数的时候,会直接进入retrun的匿名函数。接下来就是利用debounce中一直保留的词法环境写出一个严谨的防抖函数了。简单的说,就是利用这块词法环境记录上次点击的时间计算是否超出设置上限,再随便写几个if else完成。

最后感谢@花裤衩 ,让我能读到优秀的源码。

还有引用@明易 的《JS设计模式三:模块模式》链接:juejin.cn/post/684490…