add(1,2)(3)(4,5,6)的正确写法

add(1,2)(3)(4,5,6)的正确写法

这两年一直有个面试题,我班上的同学出去面试时也经常会遇到,就是实现一个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循环,上课也经常讲错,但单就这个问题来说,我还是有自信的,当社区中有不少人理解错误并且没有被纠正,甚至有可能对其它人产生误导时,我觉得还是有必要写一下的,希望不要被盯着那句“很多写法不完全正确”,还望各位见谅。

以上。

发布于 2020-11-16 22:38