柯里化函数的实现和应用

一、什么是柯里化?

柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。

基维百科上的解释是:

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

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

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

// Currying后
function curryingAdd(x) {
	return function(y){
		return x + y;
	}
}

console.log(add(1, 2));          //3
console.log(curryingAdd(1)(2));  //3

上面代码中,实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y。

二、举例柯里化函数思想实现的场景

1.参数复用,减少重复传递的参数

// 正常正则验证字符串 reg.test(txt)
function check(reg, txt) {
    return reg.test(txt)
}

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

// Currying后
function curryingCheck(reg) {
	return function(txt) {
		return reg.test(txt)
	}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

console.log(hasNumber('test1'))      // true
console.log(hasNumber('testtest'))   // false
console.log(hasLetter('21212'))      // false

上面代码是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字、是否有字母,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2.提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener;
    if (isSupport) {
        return element.addEventListener(event, handler, false);
    } else {
        return element.attachEvent('on' + event, handler);
    }
}

上面代码中,第一种写法比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

3.延迟运行

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
 
    return function() {
        return _this.apply(context, args)
    }
}

像我们js中经常使用的bind,实现的机制就是Currying。

补充:

JavaScript中的 Array.prototype.slice.call(arguments) 能将有length属性的对象转换为数组。

三、通用的封装方法

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)©这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

// 支持多参数传递
function progressCurrying(fn, args) {
    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

四、性能

函数柯里化可以用来构建复杂的算法和功能,但是滥用也会带来额外的开销。从上面部分代码中可以看到,使用柯里化函数,离不开闭包、arguments、递归。

  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上;
  • 存取arguments对象通常要比存取命名参数要慢一点,一些老版本的浏览器在arguments.length的实现上是相当慢的;
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点;

五、扩展一道面试题

实现下面这个方法:

add(2)(1, 3, 4)(2, 3)(3)(4, 6)(7, 98)() // 133

上面这个函数当参数为空的时候执行了内部参数所有值的相加,所以我们应该考虑当参数不为空的时候将缓存起来,在为空的时候再相加,这样的思路会用闭包的方式来实现。实现方法:

function add () {
  // 用来缓存所有的arguments值  
  let args = [].slice.call(arguments);
  // 新建currying函数实现柯里化  
  let currying = function () {
    // 如果参数为空,那么递归停止,返回执行结果
    if (arguments.length === 0) {
      return args.reduce((a, b) => a + b);
    } else {
      // 否则将参数保存到args里面,返回currying方法
      args.push(...arguments);
      return currying
    }      
  }
  return currying
}

上面有需要注意的一点,因为currying函数里面使用arguments,所以currying不能使用箭头函数,箭头函数内部的arguments的用法与箭头函数内部的this差不多,它取的是上一级函数的arguments值。如果想用箭头函数,currying函数可以这样改动:

let currying = (...rest) => {
  // 如果参数为空,那么递归停止,返回执行结果
  if (rest.length === 0) {
    return args.reduce((a, b) => a + b);
  } else {
    // 否则将参数保存到args里面,返回currying方法
    args.push(...rest);
    return currying
  }      
}

参考文章

https://www.jianshu.com/p/2975c25e4d71
https://www.cnblogs.com/kdcg/p/10192421.html
https://www.cnblogs.com/wphl-27/p/10336591.html

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值