前言
函数柯里化:将多参简化为单参数的一种技术方式,其最终支持的是方法的连续调用,每次返回新的函数,在最终符合条件或者使用完所有的传参时终止函数调用
上面这个描述,如果没有接触过”柯里化“可能有点晦涩难懂,我来白话文讲解下。
一个函数会有N多个入参,比如sum(1, 2, 3, 4, 5, 6)
多参简化为单参数,也就是转换成sum(1)(2)(3)(4)(5)(6)
,同时这也实现了方法的连续调用,相当于
sum(1)
sum(2)
...
sum(6)
结合上述例子,那么这句每次返回新的函数,在最终符合条件或者使用完所有的传参时终止函数调用就很好理解了
也就是当符合条件(意味着你可以设置条件,上述例子没有设置,接下来会新起个对应的例子描述)
或者使用完所有的传参时终止函数调用,也就是说,当你对sum()
这个函数实现了柯里化后,假设这个sum函数目前接收的入参只有6个,那么当你连续调用6次后,就该返回这6次的和了,而之前的调用都只会返回一个新的函数
应用场景
乍一看,这个柯里化似乎没想到什么非得的应用场景啊,没有应用场景,要这个技术有个啥用捏?
那么不妨看看下面几个应用例子
应用场景一 :小模块代码可重用 - 例子 购买某些商品,计算其价格
第一波迭代 商品价格 * 折扣
/**
* @params price 单价
* @params discount 折扣
* @return finalPrice 最终价格 Number
*/
function getPrice(price, discount){
return price * discount;
}
let productList = [{"price": 10, "discount": 0.4},{"price": 4,"discount": 0.2}]
let finalPriceArr = productList.map(item => getPrice(item.price, item.discount))
...
simple and easy
第二波迭代 折扣要进行某段复杂的业务逻辑计算,得出的才是最终的
(比如说折扣的数据源是外部的,大于0.9的当9折算,大于0.7小于等于0.9的当8折算什么的)
暂且认为我们已经抽离了折扣复杂计算的函数为discountComp()
此时正常逻辑我们会
- 要么传入discount前,对discount经过
discountComp()
处理 - 要么在
getPrice
函数里,对传入的discount进行处理
因为是迭代,getPrice
这个函数可能被N多个地方调用(比如说代码里有100个地方用到了),那么最小的改动,我们应该是在getPrice()
里调用discountComp()
/**
* @params discount 折扣
* @return finalDiscount 最终折扣 Number
*/
function discountComp(discount) {
// 贼拉复杂的计算
}
/**
* @params price 单价
* @params discount 折扣
* @return finalPrice 最终价格 Number
*/
function getPrice(price, discount){
const finalDiscount = discountComp(discount)
return price * finalDiscount;
}
let productList = [{"price": 10, "discount": 0.4},{"price": 4,"discount": 0.2}]
let finalPriceArr = productList.map(item => getPrice(item.price, item.discount))
...
一切都很完美,对吧,除了每次getPrice都得进行一次复杂计算,好吧,虽然我知道,但时间紧交差了(工作中绝对有过这种,对吧?)
然后就会有下面的杯具了
第三波迭代 全场一律五折促销,且折扣的复杂计算加深了,要调用某个外部数据源的接口,获得某个系数值,再进行复杂计算
再看回现有的代码,卧槽?那岂不是每一次getPrice我都要调一次接口?有个商品列表,100个商品我得调100次?这直接购物车计算就卡爆了好嘛。
当然还是有除了“柯里化”这一招以外能破的方法的,因为是常量系数,提前获取了定义为全局常量然后再进行
discountComp
函数就可以了,或者说给getPrice和discountComp加参,该参数接收接口获取到的常量系数,但是弊端也明显,要写、要改的代码想想就不少,在此不做赘述
用柯里化思维解决这个需求
/**
* @params discount 折扣
* @return finalDiscount 最终折扣 Number
*/
function discountComp(discount) {
/* 贼拉复杂的计算, 还有API请求 */
}
/**
* @params price 单价
* @params discount 折扣
* @return finalPrice 最终价格 Number
*/
function getPrice(price, discount){
return price * finalDiscount;
}
function curriedGetPrice(discount){
const finalDiscount = discountComp(discount)
return price => getPrice(price, finalDiscount)
}
let productList = [{"price": 10, "discount": 0.4},{"price": 4,"discount": 0.2}]
let afterDiscountComp = curriedGetPrice(0.5)
let finalPriceArr = productList.map(item => afterDiscountComp(item.price))
...
这个场景,要杠的话还是有可杠之处的,将就着看吧
应用场景二:不定参数累加
很好理解,实现如下:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
其实就是柯里化后的add()
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
console.log("arguments", arguments)
var _args = [].slice.call(arguments);
console.log("_args", _args)
// 在内部声明一个函数,利用闭包的特性保存_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(_args);
}
要读懂上面的代码,也是需要一定了解的
先是:
[].slice.call(arguments)
arguments对象是所有(非箭头)函数中都可用的局部变量
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments
[].slice.call()
先要知道slice()
。
介绍:slice()
方法返回一个新的数组对象,这一对象是一个由begin
和end
决定的原数组的浅拷贝(包括begin
,不包括end
)。原始数组不会被改变
入参:image.png
这里其实还涉及”浅拷贝“的知识,这就与”引用“有关系了
image.png
简单来说,如果数组里是对象,那么slice
会复制该对象的引用,此时如果对象本身的属性修改了,那么slice后的数组里的对象引用所指向的对象也会被修改,这一点对于普通的字符串、数字、布尔值来说,不适用,这三种将直接复制
然后就是image.png
然后是[].push.apply
相当于合并数组了;
再然后是_args.reduce
也即是Array.reduce
再再然后便是_adder.toString
,这里重写了_adder函数的toString方法
这有个默认规则,当我们定义了test函数,直接console.log(test)
,相当于执行该函数的默认toString方法,即返回函数代码字符串。
此处重写,当为add(1, 2)
,执行的是_adder的toString方法,而当为add(1,2)(4,5)
,此时用到了匿名函数立即执行的语法,相当于add(1,2)
执行完,得到了_adder
函数,此时立即执行_adder(4,5)
,则不会走toString先,而是执行定义好的_adder
函数,而这个函数进行了[].push.apply(_args, [].slice.call(arguments));
就很好理解了,最终又返回了个_adder
函数,但是此时_args已经累加了第二个(4,5)
,最终得到的还是_adder函数,所以最终执行了_adder的toString方法
再通俗讲:
当为add(1,2)
,只执行了_adder被重写后的toString方法
当为add(1,2)(3)...
,两个甚至更多立即执行的函数,则执行对应次数的_adder方法后,最终执行_adder被重写后的toString方法