闭包与柯里化:函数式编程的"套娃"艺术

77 阅读4分钟

程序员的世界里,函数不仅能吃参数,还能"吃记忆"——这就是闭包的魔法。而柯里化就像俄罗斯套娃,把大函数拆成小函数,让参数像快递一样一个个签收!

一、闭包:函数的"记忆面包"

在 readme.md 中,闭包被定义为可以访问自由变量的函数。举个现实例子:就像你记住咖啡店的位置(自由变量),每次点咖啡(函数执行)时都能直接找到它。

// 咖啡店定位器(闭包示例)
function createCoffeeLocator() {
  const cafe='星巴克'
  return function fn(order) { // 闭包诞生!
    return `从${cafe}${order}咖啡`;
  }
}

const getStarbucks = createCoffeeLocator();
console.log(getStarbucks("拿铁")); // "从星巴克取拿铁咖啡"

闭包三要素

  1. 外层函数
  2. 内层函数
  3. 外层函数变量(cafe被"记住")

要了解闭包,我们先来了解一些垃圾回收机制

现在常用的垃圾回收机制——标记清除法

现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。 核心:

  • 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
  • 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

image.png

再回到我们上述的代码,我们从getStarbucks出发,是能够访问到createCoffeeLocator的内部函数fn的,因此我们是访问到cafe这个变量的

如果要更加详细地了解闭包,可以看一下我的这篇文章juejin.cn/post/751005…

二、函数变形记:从"社牛"到"社恐"

JavaScript 函数有百变形态,就像人的社交状态:

1. 2.js 的 "社牛函数" - 直接接受所有参数

function add(a, b, c) {
  const args = Array.from(arguments); // 类数组转正
  return args.reduce((sum, val) => sum + val, 0);
}
console.log(add(1, 2, 3)); // 6

这里遇到个有趣问题:arguments 是数组吗?

  • 长得像数组:有 length 和索引
  • 但不是真数组:arguments.map() 会报错!
  • 转正秘籍:Array.from(arguments) 或 [...arguments]

2. 3.js 的 "轻度社恐" - Rest参数

function add(...args) { // 直接获得真数组
  return (...newArgs) => {
    return [...args, ...newArgs].reduce((a, b) => a + b);
  }
}
console.log(add(1, 2)(3, 4)); // 10

Rest 参数(...args)三大优势:

  1. 真数组!自带 map/filter 等技能
  2. 明确声明参数意图
  3. 与箭头函数完美配合(箭头函数没有 arguments

三、柯里化:函数的"分期付款"

柯里化(Currying)的本质:把多参数函数转化为单参数函数的链式调用。就像吃汉堡:

  • 普通吃法:一口咬下(eat(bread, beef, lettuce)
  • 柯里化:先吃面包,再吃牛肉,最后吃生菜(eat(bread)(beef)(lettuce)

手写柯里化函数(1.js 深度解析)

function curry(fn) {
  // 闭包开始:记住目标函数fn
  return function judge(...args) {
    // 检查参数是否凑齐
    if (args.length === fn.length) {
      return fn(...args); // 齐活!开饭!
    }
    
    // 没凑齐?继续等参数
    return (...newArgs) => {
      // 合并新旧参数,递归调用
      return judge(...args, ...newArgs);
    }
  }
}

// 使用示例
const curriedAdd = curry((a, b, c) => a + b + c);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6 (支持混合调用)

柯里化四步心法

  1. 闭包封装:用闭包保存目标函数和已收集参数
  2. 长度检测fn.length vs args.length(核心逻辑!)
  3. 参数合并:递归时用 ...args 和 ...newArgs 拼接
  4. 延迟执行:直到参数数量匹配才调用原函数

函数式编程趣闻:柯里化命名来自数学家 Haskell Curry,连编程语言Haskell都以他命名!

柯里化 vs 部分应用

常被混淆的两种技术:

特性柯里化部分应用
参数传递每次只传一个参数可一次传多个参数
返回值单参数函数链直接返回部分应用函数
典型形态f(a)(b)(c)f(a, b)(c)
目标函数分解参数预设

四、闭包陷阱:内存泄漏的"黑洞"

闭包虽好,但过度使用可能掉坑:

function createHeavyTool() {
  const bigData = new Array(1000000).fill("🚀"); // 大对象
  
  return function() {
    return bigData[0]; // 闭包引用bigData
  }
}

const getFirst = createHeavyTool();
// 即使只用bigData[0],整个数组仍存在内存中!

会造成内存泄露,占用内存空间,不使用了需要及时释放

避坑指南

  1. 及时解除引用:getFirst = null
  2. 避免循环引用

结语:拥抱"套娃"艺术

闭包和柯里化就像编程世界的俄罗斯套娃:

  • 闭包让函数拥有记忆,化身"智能包裹"
  • 柯里化将复杂任务拆解,变成参数流水线

回顾今日关键点:

  1. 闭包 = 函数 + 自由变量(词法环境)
  2. arguments 是类数组,Rest参数是真数组
  3. 柯里化核心:fn.length 参数计数 + 递归拼接

函数式编程如同搭乐高:每个小函数都是积木,闭包是积木间的卡扣,而柯里化就是设计图!下次当你看到 add(1)(2)(3),别忘了会心一笑——这代码正在玩套娃呢!