闭包:概念、应用与内存管理

理解闭包原理,掌握防抖节流实现,避免内存泄漏

问题

什么是闭包?如何用闭包实现防抖、节流和模块化?闭包会导致什么内存问题?

解答

什么是闭包

闭包是指函数能够访问其词法作用域中的变量,即使函数在该作用域外执行。

function outer() {
  let count = 0; // 外部函数的变量
  
  return function inner() {
    count++; // 内部函数访问外部变量
    return count;
  };
}

const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

inner 函数持有对 count 的引用,即使 outer 已执行完毕,count 仍然存活。

应用场景

1. 防抖(Debounce)

连续触发时,只执行最后一次。

function debounce(fn, delay) {
  let timer = null; // 闭包保存定时器 ID
  
  return function (...args) {
    // 清除上一次的定时器
    clearTimeout(timer);
    
    // 重新设置定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用示例
const handleSearch = debounce((keyword) => {
  console.log('搜索:', keyword);
}, 300);

// 模拟连续输入
handleSearch('a');
handleSearch('ab');
handleSearch('abc'); // 只有这次会执行

2. 节流(Throttle)

固定时间间隔内只执行一次。

function throttle(fn, interval) {
  let lastTime = 0; // 闭包保存上次执行时间
  
  return function (...args) {
    const now = Date.now();
    
    // 距离上次执行超过间隔时间才执行
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用示例
const handleScroll = throttle(() => {
  console.log('滚动位置:', window.scrollY);
}, 200);

window.addEventListener('scroll', handleScroll);

3. 模块化

用闭包实现私有变量和方法。

const userModule = (function () {
  // 私有变量,外部无法直接访问
  let users = [];
  let idCounter = 1;
  
  // 私有方法
  function generateId() {
    return idCounter++;
  }
  
  // 公开 API
  return {
    add(name) {
      const user = { id: generateId(), name };
      users.push(user);
      return user;
    },
    
    remove(id) {
      users = users.filter(u => u.id !== id);
    },
    
    getAll() {
      return [...users]; // 返回副本,保护原数据
    }
  };
})();

// 使用
userModule.add('Alice');
userModule.add('Bob');
console.log(userModule.getAll()); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
console.log(userModule.users); // undefined,无法访问私有变量

内存泄漏问题

闭包会阻止变量被垃圾回收,使用不当会导致内存泄漏。

// 问题示例:DOM 引用导致泄漏
function bindEvent() {
  const element = document.getElementById('button');
  const largeData = new Array(1000000).fill('x'); // 大数据
  
  element.addEventListener('click', function () {
    // 闭包引用了 largeData,即使不使用也不会被回收
    console.log('clicked');
  });
}

// 解决方案 1:及时清除引用
function bindEventFixed() {
  const element = document.getElementById('button');
  let largeData = new Array(1000000).fill('x');
  
  // 用完就清除
  processData(largeData);
  largeData = null; // 解除引用
  
  element.addEventListener('click', function () {
    console.log('clicked');
  });
}

// 解决方案 2:移除事件监听
function bindEventWithCleanup() {
  const element = document.getElementById('button');
  
  function handleClick() {
    console.log('clicked');
    // 执行后移除监听
    element.removeEventListener('click', handleClick);
  }
  
  element.addEventListener('click', handleClick);
}

关键点

  • 闭包 = 函数 + 其词法环境的引用
  • 防抖:延迟执行,重复触发会重置计时器
  • 节流:固定频率执行,忽略间隔内的重复调用
  • 模块化:利用闭包实现私有变量,只暴露公开 API
  • 内存管理:不再使用的闭包变量要手动置为 null,及时移除事件监听