js中函数柯里化究竟为何物?




简介

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

  • 是一个部分配置多参数函数的过程
  • 每一步都返回一个接受单个参数的部分配置好的函数。

可以理解成,将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用,例如f(1)(2)。其数学表达式为:

z = f(x, y) 转换成 z = f(x)(y) 的形式
// 普通的add函数
function add(x, y) {
    return x + y
}

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

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

如果用链式调用,可以这么写

// 链式调用
var d = 1;
d.add(2).add(3).add(4) //输出10

Number.prototype.add = function(x){
	return this + x;
}

这样看来,柯里化和链式调用是有异曲同工之妙的。

用es6的箭头函数语法来理解,可能更为直观一些。

let add = x => y => x + y
let add2 = add(2)

具体调用过程如下:

前 n - 1 次调用,其实是为了将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值。

所以连续箭头函数可以看做是多次柯里化函数的 es6 写法。

n 个连续箭头组成的函数实际上就是柯里化了 n - 1次,其中涉及到一些作用域链的内容,可以到参考资料中查阅了解一下或者自行搜索。

let test = a => b => c => {console.log();}
a()()()

它突出了一种思想——降低适用范围,提高适用性




题目

实现 add(1)(2)(3)

(一)粗暴版

function add (a) {
	return function (b) {
		return function (c) {
		    return a + b + c;
		}
	}
}
console.log(add(1)(2)(3)); // 6

(二)柯里化解决方案

  • 参数长度固定
const curry = (fn) => (
    judge = (...args) => args.length === fn.length  ? 
    fn(...args): 
    (...arg) => judge(...args, ...arg)
);

const add = (a, b, c) => a + b + c;
const curryAdd = curry(add);
console.log(curryAdd(1)(2)(3)); // 6
console.log(curryAdd(1, 2)(3)); // 6
console.log(curryAdd(1)(2, 3)); // 6

实现add(1, 2, 3)(4)(5)(6)

  • 参数长度不固定
function add (...args) {
    //求和
    return args.reduce((a, b) => a + b)
}

function currying (fn) {
    let args = []
    return function temp(...newArgs) {
        if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return temp
        } else {
            let val = fn.apply(this, args)
            
            //保证再次调用时清空
            args = [] 
            
            return val
        }
    }
}

let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)(5)())  //20
console.log(addCurry(1)(2)(3, 4, 5)())  //15
console.log(addCurry(1)(2, 3, 4, 5)())  //15

另一种写法

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9

应用




lodash.curry

_.curry(func, [arity=func.length])

创建一个函数,该函数接收 func 的参数,要么调用func返回的结果,如果 func 所需参数已经提供,则直接返回 func 所执行的结果。或返回一个函数,接受余下的func 参数的函数,可以使用 func.length 强制需要累积的参数个数。

_.curry.placeholder值,默认是以 _ 作为附加部分参数的占位符。

参数
  1. func (Function): 用来柯里化(curry)的函数。
  2. [arity=func.length] (number): 需要提供给 func 的参数数量。
返回

(Function): 返回新的柯里化(curry)函数。

例子
var abc = function(a, b, c) {
  return [a, b, c];
};
 
var curried = _.curry(abc);
 
curried(1)(2)(3);
// => [1, 2, 3]
 
curried(1, 2)(3);
// => [1, 2, 3]
 
curried(1, 2, 3);
// => [1, 2, 3]
 
// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]



减少重复传递不变的部分参数

function simpleURL(protocol, domain, path) {
    return protocol + "://" + domain + "/" + path;
}

上面这个函数是将三个参数生成一个完成的url.调用如下:

let myurl = simpleURL('http', 'mysite', 'home.html');
let myurl2 = simpleURL('http', 'mysite', 'aboutme.html');

然后你会发现,前两个参数保持不变,但每次调用都需要传递。所以可以对其优化,仅传递最后一个变化的参数。

function simpleURL(path) {
   return "http://mysite/" + path;
}

这种就属于硬写了,是绝对不允许的。

使用lodash.curry库函数使函数柯里化

// 避免每次调用重复传参
let myURL1 = _.curry(simpleURL)('https', 'mysite');
let res1 = myURL1('home.html');    //

console.log(res1);//https://mysite/home.html

let myURL2 = _.curry(simpleURL)('http', 'mysite');
let res2 = myURL2('aboutme.html');    //

console.log(res2);//http://mysite/aboutme.html



将柯里化后的callback参数传递给map, filter等函数。

比如我们有这样一段数据:

let persons = [{name: 'kevin', age: 11}, {name: 'daisy', age: 24}]

如果我们要获取所有的 name 值,我们一般会这样实现:

let names = persons.map(function (item) {
    return item.name;
});

可以利用柯里化改写成如下:

let getProp = _.curry(function (key, obj) {
    return obj[key]
});
let names = persons.map(getProp('name'))

getProp 函数编写一次后可以多次使用。

let persons = [{name: 'kevin', age: 11}, {name: 'daisy', age: 24}]

let getProp = _.curry(function (key, obj) {
    return obj[key]
});
let names2 = persons.map(getProp('name'))
console.log(names2); //['kevin', 'daisy']

let ages2 = persons.map(getProp('age'))
console.log(ages2); //[11,24]

在这个场景中,将callback柯里化之后,就能实现callback的复用了,而且非常灵活,这样不需要每次map计算都新写一个匿名函数,并在回调里加上特有的逻辑,导致其无法重用。




固定部分配置项

思路和减少重复传递不变的部分参数大同小异。

例如,有一个函数加载配置,前面几项固定了,而后面部分项不固定

// 举个例子 一个爬虫 
let load_config_spider = spider.load("target.com", "get", "id=1", "name=xxx")
load_config_spider("sex=m")("page=15")



参考资料

详解JS函数柯里化

JS 执行环境、作用域链、活动对象

简述几个非常有用的柯里化函数使用场景

lodash源码学习curry,curryRight

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值