函数柯里化
关于函数柯里化的问题最初是在《JavaScript忍者秘籍》中讲闭包的部分中看到的,相信很多同学见过这样一道和柯里化有关的面试题:
实现一个add函数,使得如下断言能够能够通过:
add(1)(2)(3) === 6
add(1)(2,3)(4) === 10
简单说就是实现一个求值函数,能够将所有参数相加得出结果。
分析一下:主要有两个要点——1.拿add(1)(2)(3)来说,如果想要在add(1)后再执行(2),那么需要add(1)返回一个函数才能做到,然后这个函数的入参是2,依次类推。2.这种递归什么时候是个头呢,貌似没看到有什么操作能让它停下来,这是个好问题,我们稍后解答。
根据分析,我们先实现一个add函数:
function add(){
let arr = [];
arr = arr.concat(Array.prototype.slice.apply(arguments))
let fun = function(){
arr = arr.concat(Array.prototype.slice.apply(arguments))
return fun
}
return fun
}
console.log(add(1)(2,3)(4))
这段代码在chrome中输出:
ƒ (){
arr = arr.concat(Array.prototype.slice.apply(arguments))
return fun
}
没错,和我们预期的一样...因为没法跳出递归调用,所以输入了fun函数,而且我们只是把参数存在了数组arr中,但是没有做累加计算。继续改进函数:
function add(){
let arr = [];
arr = arr.concat(Array.prototype.slice.apply(arguments))
let fun = function(){
arr = arr.concat(Array.prototype.slice.apply(arguments))
return fun
}
fun.getValue = function(){
return arr.reduce(function(total, num){
return total+num
}, 0)
}
return fun
}
console.log(add(1,2)(2,3)(4).getValue()) //12
现在可以输出正确的值了,我们给fun函数添加了一个函数getValue,用于将记录参数的数组中的元素求和。但是这样需要在add函数后调用一下getValue,这好像与需求有点差异...
valueOf()和toString()
这时候,我们想到两个方法valueOf()和toString(),这两个都是定义在Object原型上的方法,他们有什么特别吗?
Object.prototype.valueOf()
用 MDN 的话来说,valueOf() 方法返回指定对象的原始值。
JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。
记住上面这句话,下面我们会细说所谓的自动调用是什么意思。
Object.prototype.toString()
toString() 方法返回一个表示该对象的字符串。
每个对象都有一个 toString() 方法,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。
这里先记住,valueOf() 和 toString() 在特定的场合下会自行调用。
——摘自ChokCoco《一道面试题引发的对javascript类型转换的思考》
简单来说就是,这两个函数会在特定的场合下被js引擎隐式调用,当然两个函数被调用的条件是不同的,这里不展开分析,可参考《一道面试题引发的对javascript类型转换的思考》。
我们继续改造add函数:
function add(){
let arr = [];
arr = arr.concat(Array.prototype.slice.apply(arguments))
let fun = function(){
arr = arr.concat(Array.prototype.slice.apply(arguments))
return fun
}
fun.toString = function(){
console.log(222)
return arr.reduce(function(total, num){
return total+num
}, 0)
}
return fun
}
console.log(add(1,2)(2,3)(4))
先不执行它,我们来分析一下。我们重写了fun函数的toString方法,假设它会被js引擎调用,我们调用了reduce方法来为数组中的元素求和然后return出来,看起来没什么毛病对吧?
首先在chrome(62.0.3202.94)上执行一下这段代码,看到了什么?
ƒ 12
222
222
是不是很诡异?我想要的是12,这f 12是什么鬼...222输出了两遍又是什么东东,更诡异的是,先输出了f 12后输出了222...
chrome上输出f 12,我们可以写一个更简洁的函数来模拟:
var app = function(){};
app.toString = function(){
console.log('toString')
return 12
};
function app1(){
return app
};
console.log(app1())
输出结果和上面完全一样,但至于为什么先输出f 12以及为什么输出两遍222,这个需要剖析chrome底层机制了,此处不做讨论。(暂时还没搞明白,后续搞清楚了会更新上来,如果有大神清楚可以在下面评论。)
再来看FireFox(57.0)中的表现:
无语了吧,直接无视咱写的toString方法,该啥样还啥样...
再来看看IE(11):
啥也不说了,再瞅瞅node:
一脸懵逼啊...虽然不知道为什么不同环境会输出这些,但可以肯定的一点是——toString方法都没有被正常执行。所以,为了规避这个问题,我们需要让js引擎更明确地知道我们想调用toString,所以,修改一下打印语句:
console.log(''+add(1,2)(2,3)(4))
现在再看看,是不是所有环境输出都正常了:
222
12
如果你是用valueOf,也会有类似的问题,只需将打印语句改为:
console.log(+add(1,2)(2,3)(4))
console.log()和alert()
除了上述的解决方法之外,还可以使用alert函数来输出结果,即:
alert(add(1,2)(2,3)(4))
大家可自行测试,除node外,浏览器中都可以弹出“12”。这是为啥呢?alert和console.log不一样吗?
还真不一样,console.log可输入任何类型的数据,然而alert只能输出String类型的数据,所以...懂了吧?
最后建议大家平时自己写代码不要像本例这样,在toString/valueOf函数中做数值运算,而且慎用类型转换。本文作为填坑记录,有不对的地方欢迎指正,文中的问题有大神有见解可以在评论区讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。