谈谈JS函数的柯里化

748 阅读4分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

前言

柯里化原本是数学中函数的一种形式,后面扩展到计算机科学中,是把接受多个参数的函数转变为接受单一参数的函数,并且会返回一个函数,这些函数的参数接受剩余的参数,其实就是利用闭包来保存之前传入的参数,使得后续函数可以直接使用。在js中,柯里化是函数式编程的基础。

一个简单的栗子

为了更好地了解柯里化,这里通过一个求和函数sum来演示柯里化的过程:

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

根据柯里化的性质,可以知道:

sum(1,2,3) = sum(1)(2)(3) = sum(1,2)(3) = sum(1)(2,3) 

很明显,柯里化后sum返回的是一个函数,并且还得保存已传入参数的状态来供返回的函数调用,这里就使用了闭包。例如,当调用sum(1)时返回的是一个已经保存了参数1的函数,后续调用这个函数会继续使用参数1来计算结果。

柯里化的最简实现代码如下:

function sum(a) { 
  return function(b) {
    return function(c) {
        return a+b+c
    };
  };
}

上面代码是sum柯里化的一个最简单实现,只实现了sum(1,2,3) = sum(1)(2)(3)功能,但往往,柯里化会被封装成一个包装函数,通过传入一个函数,然后返回一个柯里化后的函数,例如的lodash中的_.curry

我们再来使用柯里化包装函数对sum进行改造:

function curry(f) { 
  return function(a) {
    return function(b) {
        return function(c) {
          return f(a, b, c);
        };
    };
  };
}

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

let curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3))//6

虽然实现了柯里化包装器的封装,但这还不够,它每次只能传入1个函数,无法实现参数的自由组合,不能算是完整的柯里化。

再来完善一下,来实现一个多个参数的柯里化函数包装器。

// 柯里化包装器
function curry(func) {
  return function curried(...args) { // 1
    if (args.length >= func.length) { //2
      return func.apply(this, args);
    } else { //3
      return function(...args2) { //4c
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}
  1. 柯里化包装器的任务就是将一个函数柯里化后返回
  2. 如果新函数的参数个数大于等于原函数的形参个数(Function,length),那么直接执行,对应于sum(1,2,3) = curriedSum(1,2,3)
  3. 否则递归调用柯里化函数,返回一个保存了上一个传入参数的函数。
function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6,3个参数,相当于直接调用sum
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

应用场景

上面的sum函数柯里化只是演示了柯里化的过程,其实它是柯里化的一种用途:延迟执行。实际开发中,柯里化主要用于参数复用、兼容性处理、延迟执行等等。虽然柯里化后函数明显复杂了,但是却提高了函数的适用性。

参数复用

例如我们要封装一个发起ajax请求的函数,请求有多种方法类型,我们可以分别为这些方法进行封装,但这其实里面很多逻辑都是可以复用的,这里就可以使用函数的柯里化。

function request(method,url,payload){
    const xhr = new new XMLHttpRequest(...)
    xhr.open(method, url, true); 
    xhr.send(payload);
}

function curriedRequest(method){
    return function(url,payload){
        const xhr = new new XMLHttpRequest(...)
        xhr.open(method, url, true); 
        xhr.send(payload);
    }
}

这里实现了对method参数的复用,它能返回适用于不同请求方式的函数。

const getAjax = curriedRequest('GET')
const postAjax = curriedRequest('POST')

postAjax('www.example.com',JSON.stringify({...}))
/*
...
*/

特性检测/兼容性处理

因为浏览器的发展和其他各种原因,有些函数和方法没有被部分浏览器支持,而我们写的代码需要覆盖大部分的群体,因此我们需要进行提前判断,从而确定用户的浏览器支不支持相关的方法,然后对不支持的方法进行polyfill。例如下面的事件监听,addEventListenerattchEvent分别是2种注册监听的方式,其中attchEvent只在低版本IE中实现,因此我们需要创建一个函数来进行对两者的判断。

const whichEvent = ( function () {
    // 绝大部分支持,优先判断addEventListener
    if(window.addEventListener){
        return function(element,type,listener,useCapture){
            element.addEventListener(type,function(e){
                listener.call(element,e);
            },useCapture);
        }
    // 判断IE中的attchEvent
    }else if(window.attchEvent){
        return function(element,type,handler){
            element.attchEvent('on'+element,function(e){
                handler.call(element,e);
            });
        }
    }
} )();