闭包:那个“赖着不走”的家伙,到底有什么用?

0 阅读6分钟

昨天我们认识了闭包——那个“虽然离开了家,但还记得家里密码”的神奇函数。今天咱们来深挖一下:闭包这玩意儿到底能干啥?有没有什么副作用?怎么防止它把内存吃光?看完这篇,你不仅知道闭包怎么用,还能在面试官面前侃侃而谈。

前言

闭包就像一个“赖着不走”的租客。你以为人走了,结果他还留着你的钥匙,时不时回来拿点东西。这在JavaScript里有时候特别好用,有时候又特别坑。

今天我们就来盘点闭包的几个经典应用场景,顺便聊聊怎么让它“体面退场”,别把你的内存吃光。

一、闭包的应用场景:这个“赖着不走”的家伙还挺有用

1. 模块化:私有变量与公共方法

没有ES6模块之前,闭包是JS实现模块化的主要手段。它能把内部细节藏起来,只暴露需要公开的接口。

const counter = (function() {
  let count = 0; // 私有变量,外面访问不到
  
  function increment() {
    count++;
    console.log(count);
  }
  
  function decrement() {
    count--;
    console.log(count);
  }
  
  function getCount() {
    return count;
  }
  
  return {
    increment,
    decrement,
    getCount
  };
})();

counter.increment(); // 1
counter.increment(); // 2
console.log(counter.count); // undefined,拿不到
console.log(counter.getCount()); // 2

这个模式叫IIFE(立即执行函数),它创建了一个闭包,里面的count变量被返回的方法“记住”了,外部无法直接修改,只能通过提供的接口操作。像不像一个“保险箱”?钥匙只给了你几个特定的人。

2. 函数工厂:批量生产定制函数

闭包可以用来创建带有特定“预设”的函数,比如一个能记录调用次数的函数。

function createCounter(initial = 0) {
  let count = initial;
  return function() {
    count++;
    return count;
  };
}

const counterA = createCounter(10);
console.log(counterA()); // 11
console.log(counterA()); // 12

const counterB = createCounter(0);
console.log(counterB()); // 1

每个计数器都独立拥有自己的count变量,互不干扰。这个工厂就像是做定制蛋糕,每个客户拿到的是自己专属的那一份。

3. 防抖与节流:控制函数执行频率

防抖和节流是前端性能优化的常见手法,它们的核心都依赖闭包来保存计时器和状态。

防抖:用户连续触发事件时,只有最后一次等待结束后才执行(比如搜索框输入)。

function debounce(fn, delay) {
  let timer = null; // 闭包保存timer
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

// 使用
const search = debounce(() => console.log('搜索中...'), 500);

节流:限制函数在单位时间内最多执行一次(比如滚动事件)。

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

这两个函数返回的都是闭包,里面的timerlast被“记住”了,所以每次调用都能访问到上一次的状态。

4. 柯里化:提前固定参数

柯里化是把多参数函数变成一系列单参数函数的技术,本质也是闭包。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...more) {
        return curried.apply(this, args.concat(more));
      };
    }
  };
}

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

每次返回新函数时,原来的args被闭包保存,直到参数凑齐才执行。就像是你给一家餐厅留了订单,每次打电话加菜,最后一起结算。

5. 事件监听中的回调

在事件回调里访问外部变量,其实也是闭包。比如一个简单的计数器按钮:

let count = 0;
document.getElementById('btn').addEventListener('click', function() {
  count++;
  console.log(count);
});

这里的匿名函数“记住”了外部的count变量,每次点击都能访问到最新的值。

二、闭包的“阴暗面”:内存泄漏与性能

闭包这么香,为什么还有人说它不好?因为它会“赖着不走”——那些被记住的变量,即使外部函数已经执行完了,也不会被垃圾回收,只要闭包函数还活着,它们就一直存在。

1. 什么是内存泄漏?

内存泄漏就是程序用完了内存,但系统没有及时回收,导致内存占用越来越大,最后浏览器变卡、甚至崩溃。

闭包导致泄漏的典型场景:

function leak() {
  let bigData = new Array(1000000).fill('leak');
  return function() {
    console.log('I am a closure');
    // 虽然没有直接使用bigData,但闭包还是引用了它
  };
}

const closureFn = leak(); // 泄漏了100万个元素的数组

上面这个例子中,返回的函数虽然没有用到bigData,但因为bigData和它在同一个作用域,闭包会保留整个作用域链上的所有变量。所以如果闭包一直存在,那些无用的变量也一直占用内存。

2. 如何避免闭包导致的内存泄漏?

  • 用完后解除引用:把闭包函数的变量置为null
closureFn = null; // 这样bigData就可以被回收了
  • 只保留需要的变量:如果闭包中只用到部分变量,可以用let声明在闭包外部提前“过滤”。
function good() {
  let bigData = new Array(1000000).fill('data');
  let needed = 'only me';
  return function() {
    console.log(needed); // 只引用needed,bigData会被回收
  };
}

因为闭包只引用了needed,引擎可以优化,把bigData标记为不可达。

  • 避免在循环中创建闭包(除非必要),因为循环中的闭包可能会意外持有大量变量。

3. 弱引用:救星Map和Set

ES6引入了WeakMapWeakSet,它们的键是弱引用的——如果键对象不再被其他地方引用,那么即使还在WeakMap里,也会被垃圾回收。

这在闭包中可以用来缓存数据,而不阻止回收。

const cache = new WeakMap();

function process(obj) {
  if (!cache.has(obj)) {
    const result = heavyComputation(obj);
    cache.set(obj, result);
  }
  return cache.get(obj);
}

如果obj在其他地方被销毁了,cache里的键值对也会自动消失,不会造成泄漏。

三、实战:闭包的最佳实践

  1. 用闭包封装私有数据:在不需要完全隔离的情况下,闭包是模块化的好帮手。但现代开发可以用ES6模块(import/export)替代IIFE,更清晰。

  2. 防抖节流用闭包保存状态:这是闭包的经典应用,没啥好纠结的。

  3. 谨慎使用返回闭包的高阶函数:如果闭包持有大量数据,确保及时清理。

  4. 善用let替代varlet有块级作用域,能避免一些意外的闭包问题。

  5. 在DevTools里监控内存:用Chrome的Memory面板,可以拍快照,看看哪些闭包对象一直存在,帮助定位泄漏。

四、总结:闭包是个好员工,但别让它996

闭包是JavaScript的强大特性,它让函数拥有了“记忆”,能实现模块化、柯里化、防抖节流等高级功能。但也要注意它的副作用:被记住的变量不会自动消失,如果不注意,容易造成内存泄漏。

记住几个原则:

  • 用完闭包,及时解除引用。
  • 在闭包里只引用需要的变量,减少内存占用。
  • 现代开发中,能用ES6模块就用模块,减少手动闭包模式。
  • 遇到缓存场景,优先考虑WeakMap

掌握了闭包,你就掌握了JS高级编程的核心钥匙。明天我们将走进JS的另一个灵魂领域——原型和原型链,看看那个让新手望而生畏的概念,到底是怎么一回事。

如果你觉得今天的闭包应用和内存管理讲得透彻,点个赞让更多人看到。有疑问评论区见,我们明天见!