目录
一、引入柯里化
大多数同学初次遇到柯里化都是在这几道面试题当中。
实现一个函数功能:sum(1,2,3,4…n)转化为 sum(1)(2)(3)(4)…(n)
实现一个add方法,使计算结果能够满足如下预期: add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15;
防抖和节流也是柯里化,手写防抖节流,手写反柯里化。
二、柯里化的含义
什么叫柯里化呢?
先介绍一个小的知识点:
高阶函数——>偏函数——>柯里化(包括的关系)
高阶函数:其中一个特点是可以暂存变量(∵内部有闭包)
偏函数:返回一个函数,函数的参数不止一个。
柯里化:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。(官方解释永远都是一下给你干懵,别急看完这一部分就明白了)
柯里化就是让一个函数变得更具体一些,就是使函数的作用效果变得更小。把一个函数传递多个参数完成的事情改变成一个函数传递一个参数的多个函数形式。 原则上返回的一个函数只能接收一个参数,但多个参数我们通常也叫柯里化。
【这里举个例子解释一下👇】
// 1. 我们先写一个判断数据类型的函数,返回值为Boolean
function isType(type,value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
console.log(isType('String','chailo'))
console.log(isType('Number',123))
// 2. 接下来对函数修改,返回值为函数
function isNewType(type){
return function(value){
Object.prototype.toString.call(value) === `[object ${type}]`
}
}
let isString = isNewType('String')
let isNumber = isNewType('Number')
console.log(isString('chailo'))
console.log(isNumber(123))
// 以前函数使检测各种类型变量的变量类型,现在就是只检测它是不是String类型,这个过程就是使函数更具体的过程。
怎么就使函数更具体了呢?
上面呢只是一个栗子,使函数具体的过程就是函数柯里化的过程,这个过程就是对函数参数进行处理的过程。我们再来品味刚刚的官方解释,相信一下子就明白在表达什么。通俗理解:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
为什么要柯里化呢?
可能你就会问,折腾了这么半天的目的是什么呢?感觉代码还变多了呢,本来可以一步到位的事情。之前我们在柯里化的概念中提到,柯里化是使函数变得更具体些,即作用范围更小,上面这一系列的操作不就是使函数更具体的过程。先返回一个函数,这样子我们就可以根据这个函数批量的生成一些判断类型的方法,这也就是柯里化的目的所在。(以前是判断所有类型,现在是判断一个类型)那么我们同样就知道了,反柯里化就是让函数的作用范围更大。
绕来绕去讲了这么多,对于柯里化现在一定不陌生了,上面这些无非就是在解释柯里化到底是什么。那么再首位呼应一下!
柯里化的官方解释:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。换种说法,把一个函数传递多个参数完成的事情改变成一个函数传递一个参数的多个函数形式。
三、柯里化函数
实现一个通用的柯里化函数
原理:比较参数个数:根据 调用时传递的参数 和 函数的参数 做判断,如果一致就让函数执行,不一致就返回一个函数继续传参。
关键步骤:使用一个变量(args)来记录传入的所有参数;递归调用函数
Chailo👸🏾叨叨叨:可以直接跳到3,中间这部分是当时学习过程的记录,大家可以直接食用终极版本。但是我觉得从0到1的过程比结果更重要,对于一些问题也做了注释,理解起来会更轻松一些。
1.初步实现版本:(可以不看)
function curry(fn){
let args = []; // 存储用户调用传入的参数,args.length用户传入参数个数,fn.length 函数参数的个数
// 如果传递参数个数大于等于原函数参数个数,就让原函数执行;如果小于,就返回一个函数
const inner = (...arr)=>{
args.push(...arr);
return args.length >= fn.length ? fn(...args) : (...args)=>inner(...args);
}
return inner(); // 返回inner,最后的inner就是将所有参数都是传入的fn(...args)
}
function isType(type,value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
let type = curry(isType); //第一次调用会返回一个函数
let isString = type('String') //给返回的函数传入参数type,我们需要把这个参数先缓存起来,放到args中;
// 然后就是比较参数个数,大于等于原函数参数个数,就让原函数执行;如果小于,就返回一个函数继续传参。
let result = isString('chailo') //小于就继续传入参数给返回函数,再次调用inner,将value放到args中;
// 继续比较参数个数,大于等于原函数参数个数,就让原函数执行。
干啥啥不行,写bug第一名!
上面的看上去没什么毛病,但是当你这么👇调用就出问题了!
let type = curry(isType);
let isString = type('String')
let isNumber = type('Number')
console.log(isString('chailo'))
console.log(isNumber(123)) // 出错❌
报错:isNumber is not a function,原因是'String''Number'都传入同一个args中。
2.优化版本:(可以不看)
解决一下给curry函数使用args记录所有参数的过程
function curry(fn){
// args用于存储用户调用的参数。
// let args=[]不能在返回函数外,也不能在返回函数内。在外多次函数调用的参数都放在了args里;在内每次生成空的args就没了记录数据的功能。应该放在返回函数的参数中
const inner = (args=[])=>{ // 每次curring都是新的数组
return args.length >= fn.length ? fn(...args) : (...a)=>inner([...args,...a]);
// args是当前传入参数,a是给返回的函数继续添加的参数
}
return inner();
}
let type = curry(isType) //args=[]
let isString = type('String')
let isNumber = type('Number')
console.log(isString('chailo'))
console.log(isNumber(123))
但是这个又有个小毛病,不能同时给curry函数传入fn和fn的参数,就是这么个意思👇
let type = curry(isType,'String','chailo');
console.log(isNumber(type)); // 出错❌
// 问题还是出在args=[]这个参数,但其实基本的功能已经实现
这个参数传递属实是给我搞懵,但经历这么一个过程已经把柯里化的实现过程给整的清清楚楚明明白白。
3.终极版本:
最后介绍一个最容易理解最全面的版本,参数爱咋传咋传完全不受影响。
// 原理:比较多次接受的参数总数与函数定义时的入参数量,当接受参数的数量大于或等于被 Currying函数的传入参数数量时,就返回计算结果,否则返回一个继续接受参数的函数。
// 关键步骤:使用一个变量(args)来记录传入的所有参数;递归调用函数
function curry(fn, ...args) {
return (...arr) => {
let result = [...args, ...arr]
if (result.length === fn.length) {
return fn(...result)
} else {
return curry(fn, ...result)
}
}
}
// es6 实现
function curry(fn, ...args) {
return args.length >= fn.length ? fn(...args) : curry.bind(null, fn, ...args);
}
学到这部分对于柯里化已经有了一个大概的理解。那就解决一下一开始引入的问题吧!
四、柯里化面试题
-
(参数个数确定)实现一个函数功能:👉【面试题集—No.13】柯里化面试题(参数确定)
sum(1,2,3,4…n)转化为 sum(1)(2)(3)(4)…(n)
-
(参数个数不确定)实现一个add方法,使计算结果能够满足如下预期:👉【面试题集—No.12】柯里化面试题(参数不确定)
add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15;
-
防抖和节流也是柯里化,手写防抖节流,手写反柯里化。👉【手写代码】防抖&节流
五、柯里化使用场景
“ 柯里化的定义和实现都不是最重要的,柯里化和编程语言无关,本文想要阐述的重点是:它能够解决编码和开发当中怎样的问题,以及在面对不同的问题时,选择一个合适的Currying,来最恰当的解决问题。”
👆 我最喜欢这个,折腾半天俺就想知道俺到底可以用他来干嘛。
我的理解是一种编程思想,然后我们可以把它运用在实际的开发中解决一些问题或者提高效率。其实就是针对某个需求使用函数进行包装,使它可以处理这几种情况,给这个包装的过程就叫柯里化。
那柯里化到底是用来干嘛的呢?
1. 参数复用
🌰:上文我们对判断数据类型函数,固定第一个参数'String'。
2. 函数复用
🌰:上文我们对判断数据类型函数,isString()可用于判断字符串类型。
3. 延迟执行(本质是对参数复用的改进)
🌰: 👉传送门【面试题集—No.12】柯里化面试题(参数不确定)
实现一个add方法,使计算结果能够满足如下预期: add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15;
大佬,JavaScript 柯里化,了解一下?