阅读 854

「前端发动机」从 bind 聊到 curry (柯里化)

前言

很长时间没有更新,一方面是工作比较忙,另一方面自己也处于一个学习的过程。接下来应该会逐渐恢复到稳定更新的状态,分享一些有趣的知识点以及我个人的思考。感兴趣的朋友可以关注下呀!

如果有不对的地方麻烦大家斧正,我会及时更新,感谢。

博客地址 🍹🍰 fe-code

bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被 bind 的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。 —— MDN

bind 方法想必大家都很熟悉,通常用来做 this 的绑定,它会返回一个新函数。但是还有一点,我们往往会忽略掉:其余的参数将作为新函数的参数供调用时使用

栗子:

function add(a, b) {
    return a + b;
}

const addBind = add.bind(null, 1);
addBind(2); // 3
复制代码

显而易见,bind 调用时传入的参数会在 addBind 调用时和新参数一起传入。

Polyfill

那这是怎么做到的呢?我们简单看下 bind 的 Polyfill 版本,网络上类似的实现也有很多。

Function.prototype.mybind = function(context, ...args) {
    const f = this;
    const fpro = function() {};
    const fBound = function(..._arg) {
        // fpro.prototype.isPrototypeOf(this)  判断是否是 new 调用的 fBound
        return f.apply(fpro.prototype.isPrototypeOf(this)
                ? this
                : context,
                [...args, ..._arg]);
    };
    if (this.prototype) {
        fpro.prototype = this.prototype;
    }
    fBound.prototype = new fpro();
    return fBound;
};
复制代码

可以看到,bind 实际返回的是类似于 fBound 的函数,我们简化一下看看。

// 删减部分代码
// f 
// args
const fBound = function(..._arg) {
    return f.apply(null, [...args, ..._arg]);
};
复制代码

其实就是利用闭包保存了上文的参数,最后再一起传给目标函数使用。这样如果不考虑 this,不使用 bind,我们可以直接把 add 函数改成这样:

function add(a) {
    return function (b) {
        return a + b;
    }
}
add(1)(2); // 2
复制代码

是不是很熟悉,一道很常见的面试题,这就是我们要说的柯里化。

curry

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。—— 维基百科

curry 其实是函数式编程中非常重要的一个思想,通过简单的局部调用,让函数具有预加载能力以及减少样板代码。

正因为我们有类似于add(1)(2)这样的需求,但是每次手动实现又很繁琐,所以可以使用一个特殊的 curry 函数,来帮助我们达到需求(在 lodash 等一些函数库中均有类似实现,感兴趣的同学可以去看看源码)。

简单实现

function curry(fn) {
    const len = fn.length;
    return function bindfn() {
        if(arguments.length < len) {
            return bindfn.bind(null, ...arguments); 
            // 关键:保存参数,并在调用时和后面的参数一起传入 bindfn
        } else {
            return fn.call(null, ...arguments);
        }
    }
}
复制代码

这个实现方案正是利用我们上面提到的 bind 的特性,所以我们再要实现 add 需求的时候就可以这样做:

// 普通写法
function add(a, b) {
    return a + b
}
const curryAdd = curry(add);
curryAdd(1)(2); // 3
复制代码

其他版本

不使用 bind 我们要怎么实现一个 curry 呢?

function _curry(fn) {
    const len = fn.length;
    function warp (..._arguments) {
        let _arg = _arguments;
        // 允许一次传完所有参数,虽然这和不用 curry 一样 add(1, 2);
        if (_arg.length >= len) {
            return fn(..._arg);
        }
        function fnc(...args) {
            _arg = [..._arg, ...args];
            if (_arg.length < len) { // 参数不够,继续返回函数
                return fnc;
            } else { // 参数足够,执行原函数
                return fn(..._arg);
            }
        }
        return fnc;
    }
    return warp;
}
复制代码

但是需求往往是多变的,以上两种方案,我们都是用形参和实参的个数来判断的函数是否需要执行,但是如果这种需求呢?

add(1)(2)() // 3add(1)(2)(3)() // 6,参数不确定且只有手动调用(),才会执行函数。很容易会想到需要改判断函数返回的判断条件。

function _curry1(fn) {
    function warp (..._arguments) {
        let _arg = _arguments;
        function fnc(...args) {
            _arg = [..._arg, ...args];
            if (args.length > 0) { // 通过判断当前是否传入了参数,来决定函数返回
                return fnc;
            } else {
                return fn(..._arg);
            }
        }
        return fnc;
    }
    return warp;
}
复制代码

当然,这种 add 函数本身也得做调整,用来满足任意参数累加的需求。

function addInfinity(...args) {
    return args.reduce((pre, cur) => pre + cur);
}
复制代码

应用

前面啰嗦这么多当然不是为了水这一篇文章,而且add(1)(2) 这种东西除了能应付面试还能做什么?我们真正需要的,还是实际应用到业务中。

  • request

相信大家都写过或者看过类似的代码。

function request(url, params) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(url), 1000);
    })
}
//
function getData(params) {
    return request('/api/getData', params);
}
function setData(params) {
    return request('/api/setData', params);
}
//...
复制代码

这就非常适合用 curry 来做处理,减少样板代码。

const _curryRequest = _curry(request);
const getData = _curryRequest('/api/getData'); // 默认入参
const setData = _curryRequest('/api/setData');
复制代码
  • map

数组的 map 函数大家都用过,简单看一个场景。

[1,2,3].map(x => 2 * x);
复制代码

如果希望这个是一个通用型的功能应该怎么处理呢?很容易会想到这么写。

function map(fn, arr) {
    return arr.map(fn);
}
function multiply2(x) {
    return 2 * x;
}
// map(multiply2, arr);
复制代码

好像看起来很不错,简洁易用。那换成 curry 我们来看下。

const mapMultiply2 = curry(map, multiply2);
// 可能有同学会发现,之前写的 curry 函数并不支持这种写法。没错,不过稍微处理一下就好,我这里就不做处理了,大家可以自己试试。
// mapMultiply2(arr);
复制代码

可以看见,curry 更利于我们去抽取函数,组合函数,让我们把更多的精力放在 multiply2 上。

总结

到这里,这篇文章就结束了。相信大家对 curry 也有了一些了解,比如,通过局部调用,让函数具有预加载能力以及减少样板代码。当然,关于柯里化的东西其实还很多,我这里讲的比较浅显,以后有机会再分享这方面的东西。

参考文章

  • JS 函数式编程指南

交流群

qq前端交流群:960807765,欢迎各种技术交流,期待你的加入;

微信群:有需要的同学可以加我好友(q1324210213),我拉你入群。

后记

如果你看到了这里,且本文对你有一点帮助的话,希望你可以动动小手支持一下作者,感谢🍻。文中如有不对之处,也欢迎大家指出,共勉。好了,又耽误大家的时间了,感谢阅读,下次再见!

往期文章:

感兴趣的同学可以关注下我的公众号 前端发动机,好玩又有料。

关注下面的标签,发现更多相似文章
评论