×

模块化封装,把逻辑打包成可复用的小工具

作者:Terry2025.07.06来源:Web前端之家浏览:44评论:0

JavaScript

不少刚学JavaScript的同学,总会纠结「闭包到底有啥用」,毕竟概念里的「函数嵌套+作用域保留」听着抽象,可实际项目里要是能把闭包用明白,代码逻辑能简洁不少,还能解决很多特定场景的需求,今天就借着几个真实开发里常见的场景,聊聊闭包到底咋帮我们解决问题。

早年前端还没有ES6 Module的时候,想把一段逻辑封装成独立模块,避免变量污染全局作用域,闭包就是个好办法,它能把变量和方法藏在函数作用域里,只暴露需要的接口,现在虽然模块化方案多了,但这种“局部作用域隔离”的思路,在做小工具时依然好用。

举个例子:做一个倒计时工具,要求每个倒计时实例独立运行,互不干扰,要是不用闭包,倒计时的剩余时间、定时器ID这些变量很容易变成全局变量,多个倒计时一起跑就会乱套,用闭包的话,就能把这些状态“锁”在函数里:

function createCountdown(initialTime) {
  let remaining = initialTime; // 剩余时间,闭包变量
  let timer = null; // 定时器ID,闭包变量
  return {
    start() {
      timer = setInterval(() => {
        remaining--;
        console.log(`剩余${remaining}秒`);
        if (remaining <= 0) {
          clearInterval(timer);
        }
      }, 1000);
    },
    reset(newTime) {
      remaining = newTime; // 只有内部方法能修改remaining
      clearInterval(timer);
      this.start(); // 重置后重新开始倒计时
    }
  };
}
// 创建两个独立的倒计时实例
const count1 = createCountdown(5);
const count2 = createCountdown(10);
count1.start(); // 实例1:5秒倒计时
count2.start(); // 实例2:10秒倒计时
// 3秒后重置实例1
setTimeout(() => {
  count1.reset(3); // 实例1变成3秒倒计时,和实例2互不影响
}, 3000);

这里createCountdown返回的对象,能访问闭包里的remainingtimer,但外部代码碰不到这两个变量,每个倒计时实例的状态都是独立的,既避免了全局变量污染,又保证了数据安全。

防抖节流:控制函数执行频率,优化性能

前端开发中,防抖(debounce)节流(throttle)是优化性能的常用手段——比如搜索框输入时减少请求次数、滚动事件里减少计算频率,这俩工具函数能生效,全靠闭包保存“定时器ID”“上次执行时间”这些关键状态。

场景1:搜索框防抖(用户停止输入后再发请求)

用户输入时,要是每次按键都发请求,网络和服务器压力会很大,防抖的逻辑是:“用户连续输入时不执行,等停一会儿再执行”,闭包用来保存定时器,每次输入都清空之前的定时器,重新计时:

function debounce(fn, delay) {
  let timer = null; // 闭包保存定时器ID
  return function(...args) {
    clearTimeout(timer); // 每次调用都清空之前的定时器
    timer = setTimeout(() => {
      fn.apply(this, args); // 延迟后执行目标函数
    }, delay);
  };
}
// 实际用法:给搜索框绑定输入事件
const searchInput = document.querySelector('#search');
function doSearch(value) {
  console.log(`发起搜索:${value}`);
  // 这里写fetch请求逻辑
}
searchInput.addEventListener('input', debounce(function(e) {
  doSearch(e.target.value);
}, 500));

用户快速输入时,debounce返回的函数会不断清空timer并重新计时,只有停止输入500毫秒后,才会执行doSearch

场景2:窗口resize节流(限制执行频率)

浏览器resize事件触发特别频繁,要是每次触发都执行复杂逻辑(比如重新计算布局、渲染图表),页面会很卡,节流的逻辑是:“一段时间内只执行一次”,闭包用来保存上次执行时间,每次触发时判断是否过了间隔时间:

function throttle(fn, interval) {
  let lastTime = 0; // 闭包保存上次执行时间
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) { // 间隔时间到了
      fn.apply(this, args);
      lastTime = now; // 更新上次执行时间
    }
  };
}
// 实际用法:监听窗口resize
window.addEventListener('resize', throttle(() => {
  console.log('窗口大小变化,执行resize逻辑');
  // 这里写重新计算布局、渲染的逻辑
}, 200));

这样不管resize多频繁,throttle返回的函数每200毫秒才会执行一次,既保证了逻辑及时响应,又减少了性能消耗。

迭代器与状态保存:让循环逻辑更可控

迭代器是“按步骤处理数据”的模式,比如逐行显示文本的打字机效果、分步执行动画,闭包能保存当前迭代位置等状态,让逻辑脱离传统for循环的“一次性执行”,变得更灵活。

场景1:打字机效果(逐字显示文本)

想让页面上的文字像打字机一样逐个出现,闭包可以保存“当前显示到第几个字”:

function createTypingEffect(text, speed) {
  let index = 0; // 闭包保存当前字符索引
  const container = document.querySelector('#typing');
  return function() {
    if (index < text.length) {
      container.textContent += text[index];
      index++;
      setTimeout(() => {
        this(); // 递归调用,继续下一个字符
      }, speed);
    }
  };
}
// 启动打字机效果
const startTyping = createTypingEffect('Hello, Closure!', 100);
startTyping(); // 调用后,文字会每秒显示一个字符

这里createTypingEffect返回的函数,每次调用都会更新闭包中的index,实现“逐字显示”,如果用for循环,文字会一次性出现,没法控制显示节奏。

场景2:数组迭代器(按需获取下一个元素)

传统for循环是“一次性遍历数组”,但有时候需要“手动控制迭代节奏”(比如点击按钮才显示下一个元素),闭包保存当前索引,就能实现这种“按需迭代”:

function createArrayIterator(arr) {
  let current = 0; // 闭包保存当前索引
  return function() {
    if (current < arr.length) {
      return arr[current++];
    }
    return null; // 迭代结束返回null
  };
}
const arr = [1, 2, 3, 4];
const next = createArrayIterator(arr);
// 手动控制迭代(比如按钮点击触发)
console.log(next()); // 1
console.log(next()); // 2
console.log(next()); // 3

这种模式很像ES6的Generator,但用闭包实现更轻量,适合简单场景。

私有变量保护:让对象属性“藏起来”,避免被外部篡改

JavaScript里,早年没有真正的“私有变量”(直到ES2022的私有字段),但闭包可以模拟私有属性——让变量只能被内部方法访问,外部无法直接修改,从而增强数据安全性。

场景:用户信息模块(保护密码等敏感数据)

做用户系统时,密码这类敏感信息不能让外部直接访问或篡改,用闭包把密码藏起来,只暴露验证、修改的方法:

function createUser(name, password) {
  let _password = password; // 闭包变量,外部无法直接访问
  return {
    getName() {
      return name;
    },
    checkPassword(input) {
      return input === _password; // 只有内部能访问_password
    },
    changePassword(newPwd) {
      // 可以加验证逻辑(比如密码长度)
      if (newPwd.length >= 6) {
        _password = newPwd;
        return true;
      }
      return false;
    }
  };
}
const user = createUser('Alice', '123456');
console.log(user.getName()); // Alice(允许访问)
console.log(user.checkPassword('123456')); // true(验证密码)
// 尝试直接篡改_password?没用!
user._password = 'abc'; // 这里只是给user对象新增了个属性,不是修改闭包内的_password
console.log(user.checkPassword('abc')); // false(内部_password没被改掉)
// 合法修改密码
user.changePassword('newPwd123');
console.log(user.checkPassword('newPwd123')); // true(修改成功)

这里_password是闭包变量,只有checkPasswordchangePassword能操作它,外部代码想“暴力破解”根本没门,保证了数据的封装性和安全性。

事件委托与回调增强:给事件处理加“记忆”

在事件委托(比如列表项点击)或异步回调(比如多个请求)中,闭包能保存额外数据上下文状态,让每次执行回调时,能拿到需要的信息。

场景1:列表项点击(保存数据ID)

页面里有个待办列表,每个列表项对应不同的任务ID,点击时要根据ID处理任务,闭包可以保存每个项的data-id

<ul id="todoList">
  <li data-id="1">任务1</li>
  <li data-id="2">任务2</li>
  <li data-id="3">任务3</li>
</ul>
function setupTodoList() {
  const list = document.querySelector('#todoList');
  list.addEventListener('click', function(e) {
    if (e.target.tagName === 'LI') {
      const id = e.target.dataset.id; // 获取当前项的ID
      // 闭包保存id,后续逻辑能用到
      function handleClick() {
        console.log(`处理任务${id}`);
        // 这里写发请求、更新状态等逻辑
      }
      handleClick();
    }
  });
}
setupTodoList();

点击不同列表项时,id会被闭包保存,handleClick执行时能拿到对应项的ID,避免了“所有项共享同一个ID变量”的问题。

场景2:异步请求(保存请求序号)

多个并行请求时,想知道每个请求是第几个发起的,闭包可以保存请求索引

function sendMultipleRequests(urls) {
  urls.forEach((url, index) => {
    // 闭包保存index,每个fetch的回调能拿到自己的序号
    fetch(url)
      .then(response => response.json())
      .then(data => {
        console.log(`第${index + 1}个请求结果:`, data);
      });
  });
}
const urls = ['/api1', '/api2', '/api3'];
sendMultipleRequests(urls);

forEach的回调里,index被闭包保存,每个fetchthen回调执行时,能准确知道自己是第几个请求,要是不用闭包,在异步场景下(比如用定时器模拟异步),索引很容易因为“变量共享”导致混乱。

闭包没那么难,用对场景才是关键

看了这么多场景,能发现闭包核心就是「保留作用域」——让函数执行后,内部的变量不会被垃圾回收,还能被后续的函数访问,不管是封装模块、做性能优化的工具、控制迭代过程、保护私有数据,还是给事件和回调加状态,闭包都是通过「把变量锁在函数作用域里,只暴露必要的操作入口」来解决问题。

刚开始用闭包可能会担心内存泄漏,但只要理解「闭包变量在外部引用消失后会被回收」(比如定时器清除、事件解绑后闭包不再被引用),合理使用反而能让代码更简洁可控,下次写代码碰到「需要保存中间状态」「不想让变量被全局污染」「需要复用逻辑但隔离数据」这些需求时,不妨想想闭包能不能帮上忙~

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://www.jiangweishan.com/article/JavaScriptsdfs235235.html

网友评论文明上网理性发言 已有0人参与

发表评论: