这两年一直有个面试题,我班上的同学出去面试时也经常会遇到,就是实现一个add函数(也有叫sum的,名字不一而足),它能够按下面这样的方式调用
add(1,2)(3)(4,5) // 15
实际上题目往往没有说清楚,这个调用不是直接返回15,而是当它在一个表达式中参与运算时,会被当成15来用,比如add(2) + 3得到5,或者div.innerHTML = add(1,2)(3)能让div中的内容为6。
网上有很多种写法,但到目前为止,我还没有看到过完全正确的写法,当然,也许是我看的不够多。
乍一看这就是一个柯里化,但它其实比柯里化要复杂一些,单纯的柯里化实现不了这种调用。
我们来分析一下这个函数的调用:
首先,它可以一直往后面加括号来继续传参调用(普通的柯里化是不能一直加括号来无限制增加参数的,总有个结束),而且参数的数量可以任意,那么它的每次调用就必须返回一个函数而不能真的返回一个数值。
知识点:对象(包括数组,对象,函数等)参与原始运算如算术或逻辑运算时,会无参调用其toString或者valueOf方法得到一个原始值,然后用这个原始值参与运算,这点上应该是借鉴自Java,但规则好像比Java要复杂,具体的我也没有太深究过,毕竟JavaScript里面我们很少利用这个特性(所以很多人其实不知道)。
能够持续调用,必然返回函数,能够当成数值,那只能是因为它实现了toString或者valueOf方法。
而且在我看来,返回的函数应该具有柯里化函数的特性,能够保留之前的参数,即:
const add3 = add(0, 1)(2) // add3的功能是对传入的数值加3并返回
console.log( add3(2) + 0 ) // log出5
const add8 = add3(1)(2)(2) // add8由add3的持续调用得到
const add8p = add3(5) // 另一种方式得到add8,注意两个add8不是同一个函数,起名add8p
const add9 = add8(1) // 由add8再传入得到add9函数
console.log( add9(1) + 3 ) // log出13
console.log( add8(1) + 3 ) // log出12
/*
注意以上代码中的add,add3,add8,add8p,add9都是不同的函数,且每个函数要加的数是不一样的。
*/
我所说的网上很多写法不完全正确,就是指没有考虑到这种情况,而只考虑了单次连续调用时能给出正确结果,但实际上真要业务中有这么个函数,应该且必须允许这种调用方式的存在。
很多写法返回的函数一直是同一个,就是那个add它自己,这就注定无法支持上面的使用方式,写法大体是把每次传入的参数push到同一个数组里面,然后在toString时求和并返回,也有每次调用时直接求出值并存下来,toString时直接返回。
还有的写法呢,总共两个函数,add是一个,后续的调用返回另一个,也同样无法支持上面的用法,代码差不多。
有兴趣的同学可以拿上面的代码测试一下其它的写法。
下面给出我的写法:
function add(...args) {
// 将参数绑定到add上
// 此时f其实还是add函数,但已经固定了一些参数,所以并不是原来的add函数
// 用bind返回新函数以保证满足**柯里化保留参数**的特性
var f = add.bind(null/*this不用绑定*/, ...args)
// 重新实现这个bound add函数的toString方法
// f参与运算应该被当成args的和,与f自己再接收的参数无关
// 考虑到lazy的特性,还是需要时再计算,但又没了缓存,每次用都会重新计算
// 如果有需要,可以改成有缓存的版本
f.toString = () => {
return args.reduce((a, b) => a + b, 0)
}
return f
}
// 考虑到add可能直接被用于运算中,可以加上这句
add.toString = () => 0
这个代码我基本上只能解释成这样了,实在无法继续解释了,因为如果能理解其它不太正确的版本,那这个版本配合注释肯定也能看懂;但如果你是小白,大概率看不懂,需要去补一补高阶函数,函数绑定等知识点。
当然,这个面试题也有一些简化版本,就是只实现三次连续的调用和传参,能出结果就行,就像标题中那样。那就简单了:
const add = a => b => c => a + b + c
这就是普通的高阶函数,就不做过多解释了。
最后说些题外话,其实我是不太喜欢写这种文章的(包括最早的Promise文章,前一篇关于 React Hooks 调用顺序的专栏文章),更不愿意说出“网上很多写法不完全正确”这句话,我这人技术其实不怎么样,开个培训班也就教个简单的for循环,上课也经常讲错,但单就这个问题来说,我还是有自信的,当社区中有不少人理解错误并且没有被纠正,甚至有可能对其它人产生误导时,我觉得还是有必要写一下的,希望不要被盯着那句“很多写法不完全正确”,还望各位见谅。
以上。