掌握闭包和柯里化,从此告别平庸代码

100 阅读5分钟

闭包和柯里化是JavaScript函数式编程中的两个核心概念,理解它们不仅能让你写出更优雅的代码,还能帮你在面试中脱颖而出。今天我们就来深入探讨这两个概念,并通过实际代码案例来掌握它们的应用。

🎯 什么是闭包?

闭包是指可以访问自由变量的函数。简单来说,当一个函数能够记住并访问其词法作用域时,就产生了闭包。

在JavaScript中,闭包无处不在。每当你在函数内部创建另一个函数时,内部函数就拥有了访问外部函数变量的能力,这就是闭包的体现。

闭包的经典应用场景

function outerFunction(x) {
    // 外部函数的变量
    return function innerFunction(y) {
        // 内部函数可以访问外部函数的变量x
        return x + y;
    };
}

const addFive = outerFunction(5);
console.log(addFive(3)); // 输出: 8

在这个例子中,innerFunction就是一个闭包,它"记住"了外部函数的变量x

🔧 函数参数处理技巧

在深入柯里化之前,我们先来了解一下JavaScript中处理函数参数的几种方式:

1. 使用arguments对象

function add(a,b,c){
    // arguments 函数运行时决定,参数总管
    console.log(arguments,arguments.length);
    // 类数组,有length属性,但没有数组的所有方法
    
    // 如何将类数组转成真正的数组?
    const args = Array.from(arguments);
    console.log(Object.prototype.toString.call(args)); // [object Array]
    
    let result = 0;
    for(let i = 0; i < args.length; i++){
        console.log(args[i]);
        result += args[i];
    }
    return result;
}

console.log(add.length); // 3 (函数定义的参数个数)
console.log(add(1,2,3)); // 6

要点总结:

  • arguments是类数组对象,不是真正的数组
  • 可以通过Array.from(arguments)转换为真正的数组
  • fn.length返回函数定义时的参数个数

2. 使用ES6的rest参数

function add(...args){
    console.log(args); // 直接就是数组
    return (...newArgs) => {
       const arr = [...args, ...newArgs];
       console.log(arr);
    }
}

add(1,2,3)(4,5,6); // 展示参数收集过程

ES6的rest参数(...args)比arguments更加现代化,直接返回数组,使用更方便。

🍛 柯里化详解

什么是柯里化?

**柯里化(Currying)**是一种函数式编程技术,将接受多个参数的函数转换为一系列只接受单个参数的函数。

现实开发中,函数的参数往往是逐步收集的,就像收集"四魂之玉碎片"一样,只有集齐所有碎片才能实现最终的愿望。

手写curry函数

让我们来实现一个通用的curry函数:

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

function curry(fn){
   // fn: 最终要执行的功能函数,作为闭包中的自由变量
   // curry函数包装fn,慢慢收集参数
   
   let judge = (...args) => {
    // 任何地方都可以访问到定义时候的fn(闭包特性)
    if(args.length == fn.length){
        // 退出条件:参数收集完毕
        return fn(...args);
    }
    // 继续收集参数
    return (...newArgs) => judge(...args, ...newArgs);
   }
   
   return judge; // 返回judge函数
}

// 使用柯里化
let addCurry = curry(add);
console.log(addCurry(1)(2)(3)); // 6

柯里化的工作原理

  1. 参数收集:每次调用都收集传入的参数
  2. 条件判断:检查已收集的参数数量是否达到原函数的参数要求
  3. 递归调用:如果参数不够,返回新函数继续收集;如果够了,执行原函数

柯里化的实际应用场景

1. 配置函数复用

// 通用的API请求函数
function apiRequest(method, url, data) {
    return fetch(url, {
        method: method,
        body: JSON.stringify(data),
        headers: {'Content-Type': 'application/json'}
    });
}

const curriedApiRequest = curry(apiRequest);

// 创建专门的GET和POST函数
const get = curriedApiRequest('GET');
const post = curriedApiRequest('POST');

// 使用
get('/api/users')(null);
post('/api/users')({name: 'John', age: 30});

2. 事件处理函数

function addEventListener(element, event, handler) {
    element.addEventListener(event, handler);
}

const curriedAddListener = curry(addEventListener);
const addClickListener = curriedAddListener(document);
const addButtonClick = addClickListener('click');

// 批量绑定点击事件
buttons.forEach(button => {
    addButtonClick(button)(() => console.log('Button clicked!'));
});

🚀 进阶技巧与最佳实践

1. 灵活参数收集

你可以改进curry函数,支持一次传入多个参数:

function advancedCurry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...nextArgs) {
            return curried.apply(this, args.concat(nextArgs));
        }
    };
}

const add = (a, b, c, d) => a + b + c + d;
const curriedAdd = advancedCurry(add);

console.log(curriedAdd(1)(2)(3)(4)); // 10
console.log(curriedAdd(1, 2)(3, 4)); // 10
console.log(curriedAdd(1)(2, 3, 4)); // 10

2. 性能优化考虑

对于频繁调用的函数,可以考虑缓存机制:

function memoizedCurry(fn) {
    const cache = new Map();
    
    return function curried(...args) {
        const key = args.join(',');
        
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        if (args.length >= fn.length) {
            const result = fn.apply(this, args);
            cache.set(key, result);
            return result;
        }
        
        const partial = function(...nextArgs) {
            return curried.apply(this, args.concat(nextArgs));
        };
        
        cache.set(key, partial);
        return partial;
    };
}

💡 总结

闭包的核心价值:

  • 数据封装:保护私有变量不被外部访问
  • 状态保持:函数执行完毕后仍能访问其作用域内的变量
  • 模块化编程:创建独立的功能模块

柯里化的应用优势:

  • 参数复用:预设部分参数,创建特定用途的函数
  • 延迟执行:参数收集完成后才执行,提供更灵活的调用时机
  • 函数组合:更容易进行函数的组合和管道操作

实战建议:

  1. 理解概念:深入理解闭包和柯里化的工作原理
  2. 场景识别:在实际开发中识别适合使用这些技术的场景
  3. 性能考量:注意闭包可能带来的内存泄漏问题
  4. 渐进应用:从简单场景开始,逐步在项目中应用这些技术

掌握了闭包和柯里化,你就拥有了JavaScript函数式编程的两大利器。它们不仅能让你的代码更加优雅和可维护,还能帮你在技术面试中展现深厚的JavaScript功底。