前言
很长时间没有更新,一方面是工作比较忙,另一方面自己也处于一个学习的过程。接下来应该会逐渐恢复到稳定更新的状态,分享一些有趣的知识点以及我个人的思考。感兴趣的朋友可以关注下呀!
如果有不对的地方麻烦大家斧正,我会及时更新,感谢。
博客地址 ?? 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)() // 3
、add(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 getData = _curry(request, '/api/getData'); // 默认入参
const setData = _curry(request, '/api/setData');
可能有同学会发现,之前写的 curry 函数并不支持这种写法。没错,不过稍微处理一下就好,我这里就不做处理了,大家可以自己试试。
- 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);
// mapMultiply2(arr);
可以看见,curry 更利于我们去抽取函数,组合函数,让我们把更多的精力放在 multiply2 上。
总结
到这里,这篇文章就结束了。相信大家对 curry 也有了一些了解,比如,通过局部调用,让函数具有预加载能力以及减少样板代码。当然,关于柯里化的东西其实还很多,我这里讲的比较浅显,以后有机会再分享这方面的东西。
参考文章
- JS 函数式编程指南
交流群
qq前端交流群:960807765,欢迎各种技术交流,期待你的加入;微信群:有需要的同学可以加我好友(q1324210213),我拉你入群。
后记
如果你看到了这里,且本文对你有一点帮助的话,希望你可以动动小手支持一下作者,感谢?。文中如有不对之处,也欢迎大家指出,共勉。好了,又耽误大家的时间了,感谢阅读,下次再见!
感兴趣的同学可以关注下我的公众号 前端发动机,好玩又有料。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。