这里说的循环调用不是指函数的递归,而是指函数的返回值仍然是函数,可以继续传参调用,如下面的代码:
function add(n){
...
}
add(1);
add(1)(2)(3);
这个循坏调用问题的来源是笔者在codewars里遇到的一道题,题目信息如下图:
翻译一下题目大意:实现add函数,函数支持累加功能,并在累加完成后返回累加结果。
笔者开始考虑的是用callee来实现,代码如下:
var result = 0;
function add(n){
result += n;
return arguments.callee;
}
对这个实现简单说明一下:
arguments是进行函数调用时,除了指定的参数外,还另外创建的一个隐藏对象(例如上面调用函数add时,除了显式的参数n外还会额外创建一个隐藏参数对象arguments,且这个隐藏参数对象arguments只能在调用的函数add内部使用),这个隐藏对象代表正在执行的函数和调用它的函数的参数。arguments.callee可以返回正在执行的函数本身,而arguments[i]则可以返回参数列表中第i个参数。
当add函数循环调用时,前一次调用完成后返回的是add函数本身,然后把后一次的参数传入,开始后一次调用。下面用几个测试用例验证一下结果是否正确:
var result = 0;
function add(n){
result += n;
return arguments.callee;
}
add(1)(2);
console.log(result);
result = 0;
add(3)(4);
console.log(result);
验证结果如下:
累加功能验证没有问题,如果我们是实现一个业务功能,那么这么写也没毛病,只是每次循环调用完成后都要去result里取值。不过codewars的用例是肯定跑不过的,因为每次函数调用返回的都是一个函数,而不是一个值(如add(1)(2)(3)的返回结果期望是6,但上面的实现返回的则是一个add函数对象)。
再考虑一下,估计很多同学都会有下面这个哥们的问题:
翻译一下这位暴躁老哥的回复重点:他无法理解同一个函数的返回结果既是一个值又是一个对象——函数add的返回怎么可以既是一个值,又可以作为一个函数传入参数执行呢?换句话说,就是最后一次循环调用需要返回一个值,除最后一次循环调用外其他调用需要返回一个可以传入参数的函数。
一个函数的返回结果当然不可能既是一个值又是一个对象,所以我们需要从另一个角度来考虑这个问题:当函数本身需要作为一个值加入运算时(在题目中就是作为一个数值),能否自动把函数转换为期望的数据类型呢?
当然是可以的,就是重写add函数返回对象的valueof方法。
下面先对valueof方法作一个简单说明:
无论是一元加号(用于数值运算)还是二元加号(用于字符串拼接),都会尝试将对象转为期望的数据类型,无论是一元加号还是二元加号,首先会调用对象的valueof方法(valueof是从object继承过来,默认返回对象本身),如果valueof方法的返回结果是基本数据类型,则会用这个值,如果是对象则会继续调用对象的toString方法,如果toString方法的返回结果是基本数据类型,则会用这个值,否则报错。
重写valueof方法的实现代码如下:
function add(n){
var fn = function(x) {
return add(n + x);
};
fn.valueOf = function() {
return n;
};
return fn;
}
继续用几个测试用例验证一下结果是否正确:
function add(n){
var fn = function(x) {
return add(n + x);
};
fn.valueOf = function() {
return n;
};
return fn;
}
console.log(add(1)(3));
console.log(add(4)(5));
验证结果如下:
累加功能验证通过。简单地说,add(1)(3)的返回结果实际上是在函数add内定义的fn对象,当我们用console.log打印返回结果或者把返回结果用于一元运算时,会隐式调用fn对象的valueof方法进行转换。