Skip to content

🔥函数式与闭包的共生关系

——柯里化、组合、惰性求值都依赖闭包,但需警惕内存泄漏

“闭包是函数式的氧气:
没有它,一切高级抽象都会窒息。”

一、闭包:函数式编程的基石

什么是闭包?

函数记住并访问其词法作用域,即使在该作用域外执行。

js
function outer(x) {
  return function inner(y) {
    return x + y; // inner 访问 outer 的 x
  };
}

const add5 = outer(5);
add5(3); // 8 —— x 仍被保留

inner 函数“封闭”了变量 x,形成闭包。

二、函数式三大技术如何依赖闭包

1. 柯里化(Currying):参数的逐步绑定

js
const add = x => y => x + y;
const add10 = add(10); // 闭包保存 x = 10
add10(5); // 15
  • add(10) 返回的函数通过闭包捕获 x
  • 实现了“部分应用”(Partial Application)

闭包让柯里化成为可能

2. 函数组合(Composition):管道的构建

js
const compose = (f, g) => x => f(g(x));

const toUpper = s => s.toUpperCase();
const exclaim = s => s + '!';

const shout = compose(exclaim, toUpper);

shout('hello'); // 'HELLO!'
  • compose 返回的函数通过闭包捕获 fg
  • 无需全局变量,组合逻辑被封装

闭包让组合函数“记住”其组件

3. 惰性求值(Lazy Evaluation):延迟计算

js
const lazyMap = (arr, fn) => {
  let result;
  return () => {
    if (!result) {
      result = arr.map(fn); // 只计算一次
    }
    return result;
  };
};

const expensiveCalc = lazyMap([1,2,3], x => x ** x);
// 未执行

expensiveCalc(); // 现在计算
expensiveCalc(); // 直接返回缓存结果
  • 闭包保存了 resultarrfn
  • 实现了“记忆化”(Memoization)

闭包是惰性求值的存储容器

三、闭包的“黑暗面”:内存泄漏

闭包强大,但会延长变量生命周期,可能导致内存泄漏。

危险案例 1:事件监听未清理

js
function setupHandler() {
  const hugeData = new Array(1000000).fill('data');
  
  window.addEventListener('click', () => {
    console.log(hugeData.length); // 闭包引用 hugeData
  });
}

setupHandler();
// 即使 setupHandler 执行完,hugeData 仍驻留内存

危险案例 2:循环中的闭包

js
function createHandlers() {
  const handlers = [];
  for (var i = 0; i < 10; i++) {
    handlers[i] = function() {
      return i; // 闭包引用 i
    };
  }
  return handlers;
}

// 所有函数都返回 10!
// 且 i 无法被回收

(注:用 let 可修复,但每个 i 仍被闭包捕获)

四、安全使用闭包的实践

1. 及时解除引用

js
function safeHandler() {
  const data = fetchData();
  
  const handler = () => {
    console.log(data.process());
  };
  
  window.addEventListener('click', handler);
  
  // 清理时
  return () => {
    window.removeEventListener('click', handler);
    // data 仍被闭包持有,但无外部引用后可被回收
  };
}

2. 避免在闭包中存储大对象

js
// ❌ 危险
const cache = {};
const getData = id => {
  if (!cache[id]) {
    cache[id] = fetchHugeData(id); // 闭包 + 全局缓存
  }
  return cache[id];
};

// ✅ 改进:使用 WeakMap 或限制缓存大小

3. 使用 WeakMap / WeakSet 存储关联数据

js
const cache = new WeakMap();

const process = obj => {
  if (!cache.has(obj)) {
    cache.set(obj, expensiveCalc(obj));
  }
  return cache.get(obj);
};
// 当 obj 被回收,cache 中对应项自动清除

五、函数式风格如何缓解内存问题

1. 纯函数减少闭包依赖

js
// 闭包方式(有状态)
const createCounter = () => {
  let count = 0;
  return () => ++count;
};

// 纯函数方式(无闭包)
const increment = n => n + 1;
// 状态由外部管理

2. 不可变数据降低副作用

js
// 闭包修改外部变量 ❌
let result = [];
const process = arr => {
  arr.forEach(x => result.push(x * 2));
};

// 不可变方式 ✅
const process = arr => arr.map(x => x * 2);
// 无共享状态,闭包更安全

六、工程启示

何时安全使用闭包?

  • 柯里化、组合、装饰器等高阶函数
  • 私有变量模拟(模块模式)
  • 事件处理器(确保清理)

何时警惕?

  • 存储大型数据结构
  • 长生命周期对象中的闭包
  • 循环中创建大量闭包函数

结语:闭包是双刃剑

它是函数式编程的“引擎”,
也是内存泄漏的“温床”。

柯里化、组合、惰性求值——这些优雅的抽象,
都建立在闭包提供的“记忆能力”之上。

但记住:

用闭包封住逻辑,别让它困住内存。

合理使用,闭包就是最强的工具;
滥用,它就会成为性能的黑洞。