JS - 函数柯里化

一、概念

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。

简单来说,柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c)转换为可调用的 f(a)(b)(c)

柯里化的3个常见作用:

  • 参数复用
  • 提前返回
  • 延迟计算/运行

二、参数复用

参数复用可以用在各种需要正则检验的需求,比如校验电话号码、校验邮箱、校验身份证号、校验密码等。

这时我们会封装一个通用函数 checkByRegExp ,接收两个参数,校验的正则对象和待校验的字符串。

普通方法,如果需要多次校验那么就调用多次方法

function checkByRegExp(regExp,string) {
    return regExp.test(string);  
}

checkByRegExp(/^1\d{10}$/, '12345678900'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '12345678900'); 
checkByRegExp(/^1\d{10}$/, '12345678900'); 

柯里化写法

//进行柯里化
let _check = curry(checkByRegExp);
//生成工具函数,验证电话号码
let checkCellPhone = _check(/^1\d{10}$/);

checkCellPhone('12345678900'); // 校验电话号码
checkCellPhone('12345678900'); // 校验电话号码
checkCellPhone('12345678900'); // 校验电话号码

可以借助柯里化对 checkByRegExp 函数进行封装,以简化代码书写,提高代码可读性。

三、提前返回

关于提前返回,这里引用一个网上流传的例子:

兼容现代浏览器以及IE浏览器的事件添加方法。以下是正常写法。

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 if …,如果使用柯里化,那么就只会在第一次的时候走 if…else if … ,即使之后再触发事件,也不会走条件判断。

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, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();

四、延迟计算

关于延迟计算,一般情况下,函数在传入参数之后,就会立即执行,柯里化的延迟计算,就是能让函数即使传入参数,也不会立即实行,而是将参数先存储在一个数组中,等到某个时机再调用数组,将之前的参数一起计算。

var curryWeight = function(fn) {
    var _selfWeight = [];
    return function() {
        if (arguments.length === 0) {
            return fn.apply(null, _selfWeight);
        } else {
            _selfWeight = _selfWeight.concat([].slice.call(arguments));
        }
    }
};
var selfWeight = 0;
var addWeight = curryWeight(function() {
    var i=0; len = arguments.length;
    for (i; i<len; i+=1) {
        selfWeight += arguments[i];
    }
});

addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
addWeight();    //  这里才计算

console.log(selfWeight);    // 12.5

五、实现柯里化函数

实现柯里化函数可以通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数。

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

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

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

六、柯里化面试题

这里再讲以下,网上的那一道和柯里化相关的面试题,以及一个关于函数内置的toString函数的知识点。

写一个函数实现 
add(1)(2)(3)  // 6

// 实现方法
 var add = function(a) {
 	return function(b) {
 		return funciton(c) {
 			return a+b+c;
 		}
 	}
 }
  add(1)(2)(3);

上面这种写法是最简单的实现方法,但是缺点显而易见,只能实现单一一种情况,如果传入的参数不定,那么就无法使用这种方法,这里附上一种写法,可以适应多个参数的情况。

var add = function(a) {
    var sum = a;
    var addMore = function(b) {
        sum += b;
        return addMore;
    };
    addMore.toString = function() {
        return sum;
    };
    return addMore;
};
var a = add(1)(2)(3)(4).toString();  //10

这里需要提一下,如果是单纯的,
var a = add(1)(2)(3)(4);
这样输出的是一个函数式

{ [Function: addMore] toString: [Function] }

必须得转化以下,才能够输出数字。另外,还有一种情况:

var add = function(a) {
    var sum = a;
    var addMore = function(b) {
        sum += b;
        return addMore;
    };
    addMore.toString = function() {
        return sum;
    };
    return addMore;
};
var a = add(1)(2)(3)(4)+10;  //20

这是由于函数的隐式转换。当我们直接将函数参与其他的计算时,函数会默认调用toString方法,直接将函数体转换为字符串参与计算。
但是,使用函数内置的 toString()函数 输出的是一个函数体字符串。例子如下,

function b() {
  return 10;
}
var c = b+10;
console.log(c);
console.log(typeof(c));
// 输出
function b() {
  return 10;
}10
string

所以,必须使用自定义的 toString 是函数返回我们想要的值。
也就是说,当没有定义toString时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。

而当我们自定义了toString方法时,那么隐式转换的返回结果则由我们自己控制了。

另外,vauleOf 的方法的作用和 toString 方法一样,但是valueOf会比toString后执行。

还有一个注意的点,

add (2,3) arguments.length == 2
add (2)(3) arguments.length == 1

柯里化面试题还有另一种形式

var a = add(1)(2)(3)(4);   //10
var b = add(1, 2, 3, 4);   //10
var c = add(1, 2)(3, 4);   //10
var d = add(1, 2, 3)(4);   //10

// 实现方法
function add() {
    var _args = [].slice.call(arguments);
    var adder = function () {
        var _adder = function() {
            _args.push(...arguments);
            return _adder;
        };
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }

        return _adder;
    }
    return adder(..._args);
}

var a = add(1)(2)(3)(4,5).toString();
console.log(a); // 10

参考链接:
前端基础进阶(十):深入详解函数的柯里化
「前端进阶」彻底弄懂函数柯里化
柯里化(Currying)

函数柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。在 JavaScript 中,可以通过递归或迭代的方式手写实现函数柯里化。 以下是一个通用的手写实现柯里化函数的示例: ```javascript function curry(fn) { return function curried(...args) { // 如果提供的参数数量大于等于原始函数所需的参数数量,则执行函数 if (args.length >= fn.length) { return fn.apply(this, args); } else { // 否则返回一个新函数,继续收集参数 return function(...moreArgs) { return curried.apply(this, args.concat(moreArgs)); }; } }; } ``` ### 使用示例 定义一个简单的加法函数,并通过 `curry` 将其柯里化: ```javascript function add(a, b, c) { return a + b + c; } const curriedAdd = curry(add); console.log(curriedAdd(1, 2, 3)); // 输出: 6 console.log(curriedAdd(1)(2, 3)); // 输出: 6 console.log(curriedAdd(1)(2)(3)); // 输出: 6 ``` ### 实现原理说明 1. **`fn.length`**:获取函数的形参个数,用于判断是否收集到了足够的参数[^1]。 2. **递归调用 `curried`**:如果参数不足,则返回一个新的函数,继续等待接收参数。 3. **`apply` 方法**:当参数足够时,使用 `apply` 调用原始函数 `fn`,并传入所有已收集的参数[^1]。 ### 支持占位符的柯里化 有时希望支持占位符(如 `undefined` 或 `_`)来延迟某些参数的传入。可以对上述实现进行扩展: ```javascript const placeholder = '_'; function curryWithPlaceholder(fn) { return function curried(...args) { // 替换占位符为后续参数中的值 const filledArgs = [...args]; const remainingArgs = []; const resolvedArgs = filledArgs.map(arg => arg === placeholder ? undefined : arg); let argIndex = 0; // 构建最终参数列表 const finalArgs = []; for (let i = 0; i < fn.length; i++) { if (argIndex < resolvedArgs.length && resolvedArgs[argIndex] !== undefined) { finalArgs.push(resolvedArgs[argIndex++]); } else { return function(...moreArgs) { argIndex = 0; const combinedArgs = resolvedArgs.concat(moreArgs); return curried.apply(this, combinedArgs); }; } } return fn.apply(this, finalArgs); }; } ``` ### 使用占位符示例 ```javascript const curriedWithPlaceholder = curryWithPlaceholder(add); const partial = curriedWithPlaceholder(1, placeholder, 3); console.log(partial(2)); // 输出: 6 ``` 上述代码中,通过占位符 `_`,可以灵活地延迟某些参数的传入。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值