函数柯里化

Posted by violetks on March 1, 2023

函数柯里化(★)

1、柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单来说,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化。
2、一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值(延迟执行),而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

// 普通的 add 函数
function add(x, y) {
  return x + y;
}

// 柯里化后:先用一个函数接受 x 然后返回一个函数去处理 y 参数
function curryingAdd(x) {
  return function (y) {
    return x + y;
  }
}

add(1, 2)         // 3
curryingAdd(1)(2) // 3

3、柯里化用途:
(1)参数复用:本质上是降低通用性,提高适用性。

// 示例一:多个不同域名下的请求
function ajax(type, url, data) {
  var xhr = new XMLHttpRequest();
  xhr.open(type, url, true);
  xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但在重复调用的时候会参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")

// 利用柯里化
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");

// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");
// 示例二:多个不同校验规则的正则
function check(reg, txt) {
  return reg.test(txt)
}

check(/\d+/g, 'test')     // false
check(/[a-z]+/g, 'test')  // true

// 利用柯里化
function curryingCheck(reg) {
  return function(txt) {
    return reg.test(txt)
  }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

(2)提前返回
例如:兼容现代浏览器以及 IE 浏览器的事件添加方法,attachEvent 是 IE 的方法,addEventListener 是主流浏览器的方法。 - 只进行一次判断。

// 每次绑定事件的时候都要走一遍 if-else 的判断逻辑
var addEvent = function(el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    }
};

// 柯里化逻辑之后
// 先自执行一次,之后调用就不会需要再走 if-else 判断
var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();

(3)延迟执行:和 ES6 的bind方法一样,只返回函数体,不执行。
①最简单的实现

var curry = function (fn) {
    // args 获取第一个方法内的全部参数 - 闭包
    var args = [].slice.call(arguments, 1);
    return function () {
        // 将后面方法里的全部参数和 args 进行合并
        var newArgs = args.concat([].slice.call(arguments));
        // 把合并后的参数通过 apply 作为 fn 的参数并执行
        return fn.apply(this, newArgs);
    };
};
// 调用
let add = (a,b) => a + b;
var addCurry = curry(add, 1);
addCurry(2);
// 但是不支持 addCurry(1)(2) 形式的调用

②增加递归实现addCurry(1)(2)
【补充知识点】
JS 函数的 length 属性: length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,那些已定义了默认值的参数不算在内,比如function(xx = 0的 length 是 0。

// 第二版 - 支持多参数传递
var processCurry = function(fn, args = []) {
    let _this = this; // 保留 this 上下文
    let len = fn.length; // 函数必传的参数个数

    return function() {
        // concat 既可以拼接数字 3 也可以拼接数组 [3,4]
        let _args = [].slice.apply(arguments).concat(args);
        // 如果 concat 后的参数个数小于必传参数的 length ,则继续递归调用 - 收集参数
        if (_args.length < len) {
            return processCurry.call(_this, fn, _args)
        }
        // 参数收集完毕,则执行 fn
        return fn.apply(this, _args);
    }
}
// 测试
let multiple = (a, b, c) => a * b * c;
// 调用方式1
let processMultiple = processCurry(multiple);
let res = processMultiple(2)(3)(4); //24
// 调用方式2
let processMultiple1 = processCurry(multiple, 3);
let res1 = processMultiple1(4)(5); // 60