闭包的应用场景:封装、防抖、节流!

188 阅读6分钟

闭包是 JavaScript 的核心概念之一,它允许函数访问并记住其创建时的作用域环境,即使该函数在其原始作用域外执行,这种特性赋予了闭包强大的能力,使其在多种场景中发挥着关键作用。

今天我们介绍的是应用场景中的封装私有变量、函数防抖和函数节流,下面,我将通过结合代码示例对此进行详细解析。


一、什么是闭包?

闭包是JavaScript中一个重要的概念,它指的是一个函数与其周围状态(词法环境)的组合。简单来说,闭包让函数可以"记住"并访问它创建时的作用域,即使这个函数在其原始作用域外执行。

闭包的作用:

  1. 记忆函数:闭包能保存函数创建时的变量状态,使嵌套函数能访问外部变量
  2. 数据封装:闭包支持创建私有变量和函数
  3. 状态保持:闭包能延长局部变量的生命周期

二、封装私有变量:解决数据安全问题

1. 封装之前:人人都可修改,数据暴露无遗

先来看一段简单的计数器实现:

let counter = {
  count: 0,
  increment() {
    this.count++;
  },
  decrement() {
    this.count--;
  }
};

// 外部可以直接修改数据
counter.count = 1000;
console.log("当前计数:", counter.count); // 输出 1000

上面这段代码虽然实现了基本的计数功能,但是,其中存在严重的 数据安全性问题,如:

  • 数据暴露无遗count 是公开属性,外部可以随意修改为任何值(如负数、字符串等)。
  • 缺乏校验机制increment 和 decrement 方法没有做参数检查或边界控制。
  • 业务规则被破坏:比如库存数量、购物车商品数等关键数据可能因此被篡改,导致系统异常。

2. 封装之后:闭包保护数据,真正达成私有

我们通过函数作用域 + 闭包的方式,将 count 变成真正私有的变量,只能通过指定的方法访问和修改。

function CreateCounter(num) {
  // 私有变量,外界无法直接访问
  let count = num;
  return {
    num,  // 公共属性,可读不可写
  
    increment: () => {
      count++;
    },

    decrement: () => {
      if (count > 0) {
        count--;
      }
    },

    getCount: () => {
      console.log("count 被访问了!!!");
      return count;
    }
  };
}

运行结果:

const counter = CreateCounter(0);

counter.increment();
counter.increment();
counter.decrement();

console.log(counter.getCount()); // 输出 1
console.log(counter.num);        // 输出 0(只读)
console.log(counter.count);      // 输出 undefined(无法访问私有变量)

从上面的案例可以看出,闭包是 JavaScript 中非常强大的特性,它能够让函数能够“记住”并访问其创建时所处的作用域,即使该函数在其外部执行。

闭包的作用:

(1)实现真正的“私有变量”

  • count 定义在 CreateCounter 函数内部,外部无法直接访问。
  • 唯一能操作 count 的方式是通过返回对象中的方法(如 incrementdecrementgetCount),这就形成了数据的封装和访问控制。

(2)延长变量生命周期

  • 即使 CreateCounter 函数已经执行完毕,count 这个局部变量仍然不会被垃圾回收,因为它被闭包函数引用。
  • 换句话说,这些方法“记住”了它们出生时的环境,并持续保留着对 count 的访问权限。

(3)对外提供受控接口

  • 用户不能直接修改 count,只能通过调用方法间接操作。
  • 我们可以在方法内部加入校验逻辑(例如防止负数),从而确保数据始终合法。

三、函数防抖:解决搜索建议的"疯狂请求"问题

1.防抖之前:搜索过度热情,请求积极响应

在实现实时搜索时,传统方法一般会导致过度请求,如以下代码,在输入一个字符串时,每按下一个字母它就会发送1次请求,而这也导致造成了大量的 资源浪费、响应顺序错乱和页面卡顿的现象

 let inputA = document.getElementById('inputA')
 
 function ajax(cotent) {
        console.log(cotent)
 }
 
 inputA.addEventListener('keyup', function (event) {
    ajax(event.target.value)
})
a99k5-eq7mt.gif

2. 防抖之后:搜索固定响应,对你定时搭理

通过使用闭包,我们就能使其在单位时间之内只执行一次,而其他时候不执行,大大优化了资源使用:

let inputB = document.getElementById('inputB')
function ajax(cotent) {
        console.log(cotent)
    }
function debounce(fn, delay) { 
            return function (args) {
                  if (fn.id) {
                     clearTimeout(fn.id)
                 } 
                clearTimeout(fn.id)
                fn.id = setTimeout(function () {
                    fn(args)
                }, delay)
            }
        }
let dobounceAjax = debounce(ajax, 1000)
        inputB.addEventListener('keyup', function (event) {
            dobounceAjax(event.target.value)
        }
        )
2fn18-uy71j.gif

可以看到,在优化后,用户在输入框中快速连续输入时,代码不会每次都立刻执行 ajax 请求,而是在用户停止输入一秒后才真正触发一次请求。

闭包的作用

(1)保持定时器状态:通过闭包保存定时器ID (fn.id),使得每次调用函数时可以取消前一个定时器并设置新的定时器,确保只有在用户停止输入超过设定延迟(如1秒)后,才会执行目标函数。

(2)提供封装性:保护内部使用的变量不被外部访问或修改,减少全局命名空间污染。

简而言之,闭包帮助实现了防抖功能,优化了资源使用和性能。


四、函数节流:解决滚动加载或窗口调整的“性能过载”问题

1. 节流之前:事件频频触发,代码跑到累死

在网页中,有些事件会被高频触发,比如:窗口大小调整(resize)、页面滚动(scroll)和鼠标移动(mousemove)事件。

例如,下面这段代码会在每次窗口大小变化时都执行一次操作:

window.addEventListener('resize', () => {
    console.log("窗口你变了!");
});
wxqpn-0fm44.gif

如果用户快速拖动窗口大小,这个回调会频繁执行,这就可能会造成大量的 DOM 操作、页面布局重新计算(reflow)和性能下降甚至卡顿,导致 浪费资源、效率低下


2. 节流之后:任你如何催促,慢跑连带散步

通过下面这段代码,我们可以通过 函数节流(Throttle) 的方式,让某个函数 每隔一段时间只执行一次,即使它被频繁调用,我们也是每隔单位时间只执行一次,其他的则会被延迟执行。

function throttle(fn, delay) {
    let last = 0;
    return function (...args) {
        const now = Date.now();
        if (now - last >= delay) {
            fn.apply(this, args);
            last = now;
        }
    };
}

// 使用节流包装一个窗口大小监听函数
const smartResize = throttle((width) => {
    console.log("当前窗口宽度:", width);
}, 500);

// 绑定 resize 事件
window.addEventListener('resize', () => {
    smartResize(window.innerWidth);
});
2kkcz-psut3.gif

闭包的作用:

(1)throttle 函数的作用

  • 接收两个参数:

    • fn:需要节流的目标函数(比如打印窗口宽度)
    • delay:两次执行之间的最小间隔时间(单位是毫秒)
  • 返回一个新的函数,这个函数才是真正被调用的“节流版”函数。

(2)关键变量:last 和它的命运

let last = 0;
  • 这个变量记录上一次函数执行的时间戳。
  • 它定义在 throttle 函数内部,按理说在函数执行完后应该被销毁。
  • 但由于返回的函数引用了它,所以它不会被垃圾回收,这就是闭包的力量!

(3)返回的函数

return function (...args) {
    const now = Date.now();
    if (now - last >= delay) {
        fn.apply(this, args);
        last = now;
    }
};
  • 每次调用都会检查当前时间和上次执行时间的差值。
  • 如果超过设定的 delay,才允许执行目标函数,并更新 last
  • 否则就跳过本次调用,避免重复执行。

五、 总结一句话

通过 函数节流 + 闭包 的方式,我们可以有效控制高频事件的执行频率,避免不必要的性能开销。

其中,闭包让函数能够记住和操作之前的状态(如上次执行时间),是实现节流机制的核心所在。