深入详解JS函数的柯里化
本文深入解析JavaScript中函数的柯里化技术。首先通过函数隐式转换与toString/valueOf优先级机制,梳理基础概念;接着演示如何封装数组map方法,引入回调与call/apply的应用。核心部分详细拆解柯里化的实现逻辑,从简单累加到通用参数收集,结合闭包特性讲解部分求值原理,并提供通用柯里化函数与bind方法的实现思路。
一、补充知识点之函数的隐式转换
先来看一个简单的思考题。
function fn() {
return 20;
}
console.log(fn + 10); // 输出结果是多少?
稍微修改一下代码,再思考输出结果会是什么?
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
console.log(fn + 10); // 输出结果是多少?
还可以继续对代码进行修改。
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
fn.valueOf = function() {
return 5;
}
console.log(fn + 10); // 输出结果是多少?
// 输出结果分别为
function fn() {
return 20;
}
10
20
15
当使用console.log输出,或者进行数学运算时,可能会触发隐式转换。从上述三个例子中可以总结出关于函数隐式转换的一些规律。
如果没有重新定义toString和valueOf方法,函数的隐式转换会调用默认的toString方法,它将函数的定义内容作为字符串返回。如果主动定义了toString或valueOf方法,则隐式转换的返回结果由开发者控制,其中valueOf的优先级略高于toString。
因此,上述例子的结论就非常容易理解了。建议大家动手尝试运行一下。
二、补充知识点之利用call/apply封装数组的map方法
map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的新数组。
通俗来说,就是遍历数组的每一项元素,在map的第一个参数(回调函数)中进行运算处理后返回计算结果,最终返回一个由所有计算结果组成的新数组。
// 回调函数中有三个参数
// 第一个参数表示newArr的每一项,第二个参数表示该项在数组中的索引值
// 第三个表示数组本身
// 除此之外,回调函数中的this,当map不存在第二参数时,this指向丢失,当存在第二个参数时,指向改参数所设定的对象
var newArr = [1, 2, 3, 4].map(function(item, i, arr) {
console.log(item, i, arr, this); // 可运行试试看
return item + 1; // 每一项加1
}, { a: 1 })
console.log(newArr); // [2, 3, 4, 5]
在上述例子的注释中详细阐述了map方法的细节。现在面临一个难题,就是如何手动封装map方法。
可以先思考for循环。我们可以使用for循环来实现一个map,但在封装时会遇到一些问题。使用for循环时,循环过程本身很好封装,但在循环内部对每一项执行的操作很难用一个固定的东西封装起来,因为每个场景下对数据的处理方式肯定不同。
于是大家想到了一个很好的办法,将这些不同的操作单独用一个函数来处理,让这个函数成为map方法的第一个参数,具体回调函数中是什么样的操作,由使用者在使用时决定。因此,根据这个思路的封装实现如下。
Array.prototype._map = function(fn, context) {
var temp = [];
if(typeof fn == 'function') {
var k = 0;
var len = this.length;
// 封装for循环过程
for(; k < len; k++) {
// 将每一项的运算操作丢进fn里,利用call方法指定fn的this指向与具体参数
temp.push(fn.call(context, this[k], k, this))
}
} else {
console.error('TypeError: '+ fn +' is not a function.');
}
// 返回每一项运算结果组成的新数组
return temp;
}
var newArr = [1, 2, 3, 4]._map(function(item) {
return item + 1;
})
// [2, 3, 4, 5]
在上述封装中,首先定义了一个空数组temp,用来存储最终的返回结果。在for循环中,每循环一次,就执行一次参数fn函数,fn的参数则使用call方法传入。
在理解了map的封装过程之后,就能明白为什么在使用map时,总是期望在第一个回调函数中有一个返回值。在eslint的规则中,如果在使用map时没有设置返回值,就会被判定为错误。
ok,明白了函数的隐式转换规则与call/apply在这种场景的使用方式,就可以尝试通过简单的例子来了解一下柯里化了。
三、由浅入深的柯里化
在前端面试中有一个关于柯里化的面试题,流传甚广。
实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6 add(1, 2, 3)(4) = 10 add(1)(2)(3)(4)(5) = 15
很明显,计算结果正是所有参数的和,add方法每运行一次,肯定返回了一个同样的函数,继续计算剩下的参数。
我们可以从最简单的例子一步一步寻找解决方案。
当只调用两次时,可以这样封装。
function add(a) {
return function(b) {
return a + b;
}
}
console.log(add(1)(2)); // 3
如果只调用三次:
function add(a) {
return function(b) {
return function (c) {
return a + b + c;
}
}
}
console.log(add(1)(2)(3)); // 6
上述封装看上去与想要的结果有点类似,但参数的使用被限制得很死,因此并不是最终想要的结果,需要通用的封装。应该怎么办?总结上述两个例子,其实我们是利用闭包的特性,将所有的参数集中到最后返回的函数里进行计算并返回结果。因此在封装时,主要目的就是将参数集中起来计算。
来看看具体实现。
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = [].slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var adder = function () {
var _adder = function() {
[].push.apply(_args, [].slice.call(arguments));
return _adder;
};
// 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
return adder.apply(null, [].slice.call(arguments));
}
// 输出结果,可自由组合的参数
console.log(add(1, 2, 3, 4, 5)); // 15
console.log(add(1, 2, 3, 4)(5)); // 15
console.log(add(1)(2)(3)(4)(5)); // 15
上述实现利用闭包的特性,主要目的是通过一些巧妙的方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。因此在调用add方法的时候,参数就显得非常灵活。当然,也就很轻松地满足了我们的需求。
那么读懂了上述demo,然后再来看看柯里化的定义,相信大家就会更加容易理解了。
柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
- 接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。
- 将部分参数通过回调函数等方式传入函数中
- 返回一个新函数,用于处理所有的想要传入的参数
在上述例子中,可以将add(1, 2, 3, 4)转换为add(1)(2)(3)(4)。这就是部分求值。每次传入的参数都只是想要传入的所有参数中的一部分。当然实际应用中,并不会常常这么复杂地去处理参数,很多时候也仅仅只是分成两部分而已。
咱们再来一起思考一个与柯里化相关的问题。
假如有一个计算要求,需要将数组里面的每一项用自定义的字符连接起来。应该怎么做?想到使用join方法,就很简单。
var arr = [1, 2, 3, 4, 5];
// 实际开发中并不建议直接给Array扩展新的方法
// 只是用这种方式演示能够更加清晰一点
Array.prototype.merge = function(chars) {
return this.join(chars);
}
var string = arr.merge('-')
console.log(string); // 1-2-3-4-5
增加难度,将每一项加一个数后再连接起来。那么这里就需要map来帮助我们对每一项进行特殊的运算处理,生成新的数组然后用字符连接起来了。实现如下:
var arr = [1, 2, 3, 4, 5];
Array.prototype.merge = function(chars, number) {
return this.map(function(item) {
return item + number;
}).join(chars);
}
var string = arr.merge('-', 1);
console.log(string); // 2-3-4-5-6
但是如果又想要让数组每一项都减去一个数之后再连接起来呢?当然和上面的加法操作一样的实现。
var arr = [1, 2, 3, 4, 5];
Array.prototype.merge = function(chars, number) {
return this.map(function(item) {
return item - number;
}).join(chars);
}
var string = arr.merge('~', 1);
console.log(string); // 0~1~2~3~4
机智的小伙伴肯定发现困惑所在了。期望封装一个函数,能同时处理不同的运算过程,但并不能使用一个固定的套路将对每一项的操作都封装起来。于是问题就变成了和封装map的时候所面临的问题一样了。可以借助柯里化来搞定。
与map封装同样的道理,既然事先并不确定将对每一项数据进行怎么样的处理,只是知道需要将它们处理之后然后用字符连接起来,所以不妨将处理内容保存在一个函数里。而仅仅固定封装连接起来的这一部分需求。
于是就有了以下的封装。
// 封装很简单,一句话搞定
Array.prototype.merge = function(fn, chars) {
return this.map(fn).join(chars);
}
var arr = [1, 2, 3, 4];
// 难点在于,在实际使用的时候,操作怎么来定义,利用闭包保存于传递num参数
var add = function(num) {
return function(item) {
return item + num;
}
}
var red = function(num) {
return function(item) {
return item - num;
}
}
// 每一项加2后合并
var res1 = arr.merge(add(2), '-');
// 每一项减2后合并
var res2 = arr.merge(red(1), '-');
// 也可以直接使用回调函数,每一项乘2后合并
var res3 = arr.merge((function(num) {
return function(item) {
return item * num
}
})(2), '-')
console.log(res1); // 3-4-5-6
console.log(res2); // 0-1-2-3
console.log(res3); // 2-4-6-8
大家能从上述例子,发现柯里化的特征吗?
四、柯里化通用式
通用的柯里化写法其实比上边封装的add方法要简单许多。
var currying = function(fn) {
var args = [].slice.call(arguments, 1);
return function() {
// 主要还是收集所有需要的参数到一个数组中,便于统一计算
var _args = args.concat([].slice.call(arguments));
return fn.apply(null, _args);
}
}
var sum = currying(function() {
var args = [].slice.call(arguments);
return args.reduce(function(a, b) {
return a + b;
})
}, 10)
console.log(sum(20, 10)); // 40
console.log(sum(10, 5)); // 25
五、柯里化与bind
Object.prototype.bind = function(context) {
var _this = this;
var args = [].prototype.slice.call(arguments, 1);
return function() {
return _this.apply(context, args)
}
}
这个例子利用call与apply的灵活运用,实现了bind的功能。
在前面的几个例子中,可以总结一下柯里化的特点:
- 接收单一参数,将更多的参数通过回调函数来搞定?
- 返回一个新函数,用于处理所有的想要传入的参数;
- 需要利用call/apply与arguments对象收集参数;
- 返回的这个函数正是用来处理收集起来的参数。
希望读完之后能够大概明白柯里化的概念,如果想要熟练使用它,就需要掌握更多的实际经验才行。
以上就是深入详解JS函数的柯里化的详细内容,更多关于JS函数的柯里化的资料请关注本站其它相关文章!
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
