Open mqyqingfeng opened 7 years ago
日常学习
好难理解啊☹️
segmentfault 的@大笑平 补充的高颜值写法:
var curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg) => judge(...args, arg)
专题系列专不知不觉更新了这么多,我得好好抽个时间系统学习一下😂
@jawil 哈哈~ 还有五篇就要完结了,可以等完结后一起看~
没有说道 什么场景下使用 柯里化
var name = person.map(function (item) {
return item.name;
})
const fn = function (item) {
return item.name;
}
person.map(fn)
PS:这样也可以参数复用
@yangchongduo 柯里化的应用更多在函数式编程中,可以看 JS 函数式编程指南,在 《JavaScript专题之函数组合》这篇文章中也有涉及部分。
@yangchongduo 这样确实可以做到参数复用,其实跟
var name = person.map(prop('name'))
是一样的,不过如果我们要获得其他属性呢?比如 'age'、'friends' 等,如果使用 fn 的话,还需要写多个 fn,而如果使用 prop 函数,就可以直接使用 prop('age') prop('friend')
高颜值写法的 judge = (...args)
用的好巧妙
function curry1(fn, args) { var length = fn.length; args = args || []; return function (...arguments) { args = [...args, ...arguments] return args.length < length ? curry1.call(this, fn, args) : fn.apply(this, args); } }
值得一看~
深受启发,这样理解柯里化 :用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数,有没有毛病
@hlmjack 正是如此~ o( ̄▽ ̄)d
在更易懂的写法里return curry.call(this, fn, _args);
,为什么要用this
呢,感觉用null
是不是也可以呢
@fi3ework 之所以写成 this 是因为希望根据环境的不同而设置不同的 this 值,我写了一个示例:
function curry(fn, args) {
var length = fn.length;
args = args || [];
return function() {
var _args = args.slice(0),
arg, i;
for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
_args.push(arg);
}
if (_args.length < length) {
return curry.call(this, fn, _args);
}
else {
return fn.apply(this, _args);
}
}
}
var fn = curry(function(a, b, c) {
console.log(this)
});
var obj = {
value: 1,
fn: fn
}
obj.fn(1, 2, 3);
这个时候的 this 的值为 obj 对象,如果设置成 null ,就会变成打印 window 对象
终于理解这句话了,“用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数”
我有个问题想请教下。 function (a, b, c){} 的参数有三个,fn('a', 'b', 'c', 'd') 正常工作,但 fn('a', 'b', 'c', 'd')('a') 就出错了。我知道这是因为 function (a, b, c){} 的参数个数,而导致的错误。 如果我想 fn('a', 'b',...n个参数)('a') 这样该如何写一个curry函数呢。
@bepromising fn('a', 'b', 'c', 'd')('a') 是因为 fn('a', 'b', 'c', 'd') 就已经执行了该函数,该函数如果没有再返回一个函数,就肯定报错,说这并不是一个函数。
至于 fn('a', 'b',...n个参数)('a'),我不清楚为什么还要再传个 'a' 呢?
柯里化 是真的绕啊
@BugHshine 是这篇写得不好,日后修订时会重写
第三版的代码是不是有bug呢,我试了一个稍微长一点的例子:
var _ = {}
var ary = curry(function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z) {
return [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z]
})
console.log(ary(1,_,_)(_)(5,6,7,8,9,10)(11,12,13,14)(15,_,_,18,_)(20,21,22,_)(24,25,26)(2,3,4)(16)(17,19)(23))
结果输出是不对的,node下:
[Function]
chrome下:
ƒ () {
var _args = args.slice(0),
_holes = holes.slice(0),
argsLen = args.length,
holesLen = holes.length,
a…
顺便我自己用es6写了一个稍微简洁一点的,对应原文第三版,可能参数多的情况下性能会有问题,但感觉一般情况下更好理解一些
// hole为自己的指定占位符
function curry(fn, hole) {
const __len = fn.length
let args = [],
return function h() {
// 先把参数放入args数组
args = [...args, ...Array.from(arguments)]
// 如果长度超过原有函数参数列表长度,表示有占位
let holeNum = args.length - __len
// 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
for (let i = 0; i < holeNum; i++) {
args[args.indexOf(hole)] = args[__len]
args.splice(__len, 1)
}
// 如果没有占位符且参数数目已经够了
if (args.length < __len || args.indexOf(hole) > -1) {
return h
} else {
return fn.apply(null, args)
}
}
}
经测试上面的例子可以输出正确的结果
@izuomeng 这个是最终的版本吗?我这边有报错
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script>
// hole为自己的指定占位符
function curry(fn, hole) {
const __len = fn.length
let args = [];
return function h() {
// 先把参数放入args数组
args = [...args, ...Array.from(arguments)]
// 如果长度超过原有函数参数列表长度,表示有占位
let holeNum = args.length - __len
// 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
for (let i = 0; i < holeNum; i++) {
args[args.indexOf(hole)] = args[__len]
args.splice(__len, 1)
}
// 如果没有占位符且参数数目已经够了
if (args.length < __len || args.indexOf(hole) > -1) {
return h
} else {
return fn.apply(null, args)
}
}
}
var _ = {};
var fn = curry(function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
});
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)
</script>
</body>
</html>
少了一行,抱歉😄
// hole为传入的占位符
function curry(fn, hole) {
const __len = fn.length
let args = [];
return function h() {
// 先把参数放入args数组
args = [...args, ...Array.from(arguments)]
// 如果长度超过原有函数参数列表长度,表示有占位
let holeNum = args.length - __len
// 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
for (let i = 0; i < holeNum; i++) {
args[args.indexOf(hole)] = args[__len]
args.splice(__len, 1)
}
// 如果没有占位符且参数数目已经够了
if (args.length < __len || args.indexOf(hole) > -1) {
return h
} else {
fn.apply(null, args)
return args = []
}
}
}
@izuomeng 这个占位的 curry 比你想的要复杂一点…… 因为对于 fn(1, _, 3)(_, 4)(2)(5);
这样的例子,结果应该是 [1, 2, 3, 4, 5],如果使用这种方法的话,结果会是 [1, 2, 3, 5, 4]
哦哦,看了几组执行结果才发现这个占位是边执行边填补的,我把他理解成从超出参数列表列表长度之后才开始填补的了,谢谢指教🙏
@izuomeng 但是你说的问题还是存在的,而且现在的写法也很繁琐,我也在想更加简洁易懂的写法~
请教下博住,第三版的填占位符是怎样一个规律。。。。
我现在才知道 function 的 length 竟然就是参数的长度,太水了 😄
很喜欢你的第二版,逻辑很清晰,很赞
@iiicon 这算是个很细的知识啦,我也是很晚才知道的~
var curry = (fn, ...args) =>
fn.length <= args.length
? fn(...args)
: curry.bind(null, fn, ...args)
var fn = curry(function(a, b, c){
console.log([a,b,c])
})
fn(1,2,3)
fn(1,2)(3)
fn(1)(2,3)
fn(1)(2)(3)
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
参考_30s.js
@Wiolem 好方法,感谢补充~ ☆´∀`☆
对于博主第二版的极简版的解释有点不太理解。 当执行 fn1()() 时,函数返回:
curry(sub_curry(function(){ return fn0() })) // 相当于 curry(function(){ return (function(){ return fn0() })() })
那么
(function(){ return fn0() })()
这个函数立即执行了,那么fn0函数也就执行了。此时工作台就因该有‘1’出现了啊。 下面是我对于博主第二版的极简版的理解:如有错误还请指出。
var fn = curry(fn0)
当执行 fn() 时,函数返回:
curry(sub_curry(fn0)) // 相当于 curry(function(){ return fn0(); })
此时我只需要把
(function(){ return fn0(); }
看做传入函数(起个名字为fn1吧)
curry(fn1); //对比curry(fn0),似是故人来。
当执行 fn()() 时,函数返回:
curry(sub_curry(fn1)) // 相当于 curry(function(){ return fn1(); })
同样可以看做传入函数(起个名字为fn2)。
curry(fn2);
当执行 fn()()() 时,同样
curry(function(){ return fn2(); })
同样可以看做传入函数(起个名字为fn3)。
curry(fn3);
当执行 fn()()()() 时,此时已不满足判断条件 if (length > 1) ,于是
return fn();
此时的传入的参数fn为fn3,
fn3() 就是 : (function(){ return fn2(); })(); fn2 =function(){ return fn1(); }; fn1 =function(){ return fn0(); }; 此时工作台才会出现‘1’
@flymie 不会执行呀
curry(sub_curry(function(){
return fn0()
}))
// 相当于
curry(function(){
return (function(){
return fn0()
})()
})
// 相当于
curry(function(){
return fn0()
})
这时候不会执行呀~
@mqyqingfeng 可能是我的描述有问题。 感觉sub_curry(fn)应该加了一条输出比较好。
function sub_curry(fn){ return function(){ console.log(1); //加了这条输出语句 return fn() } }
那么执行 fn1()()时候,
curry(sub_curry(function(){ console.log(1); return fn0() })) // 相当于 curry(function(){ return (function(){ console.log(1); return fn0() })() }) // 相当于 curry(function(){ return fnx(); //应该是相当于fnx吧。原句的这里是fn0,感觉就像函数执行过一样。 })
@flymie sub_curry返回的这个函数,在参数不全(此处是调用次数不足)的情况是肯定不会触发,因为sub_curry永远都是返回一个新的函数,而不会去执行那个函数。来完整还原一下流程(去除了if条件): 首先是fn1
curry(fn0)
// 相当于
function(){
// length = 4
return curry(sub_curry(fn0), --length)
}
然后是fn1()
(function() {
return curry(sub_curry(fn0), --length);
})()
// 相当于
curry(function(){
return fn0()
}, 3)
// 相当于
function() {
// length = 3
return curry(sub_curry(function(){
return fn0();
}), --length)
}
最后就是fn1()()了
(function() {
return curry(sub_curry(function(){
return fn0();
}), --length)
})()
// 相当于
curry(function() {
return (function(){
return fn0();
})()
}), 2)
// 相当于
function() {
// length = 2
return curry(sub_curry(function() {
return (function(){
return fn0();
})()
}), --length)
}
也就是说:只要你的调用次数没有到达规定限度,随着调用次数的增加,传给curry这个方法的函数层级会越来越深。直到调用次数够了,才会一口气将这个函数执行完,你想要的console.log(1)也才会触发
函数的length
属性获取形参的个数,但是形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
((a, b, c = 3) => {}).length; // 2
((a, b = 2, c) => {}).length; // 1
((a = 1, b, c) => {}).length; // 0
((...args) => {}).length; // 0
如果使用了ES6的函数参数默认值,可能不适用柯里化的场景。
const fn = curry((a = 1, b, c) => {
console.log([a, b, c]);
});
fn()(2)(3); // Uncaught TypeError: fn(...) is not a function
在这种情况下,期望输出的是[1, 2, 3]
,而实际上fn()
已经输出了[1, undefined, undefined]
,而不是返回闭包函数。所以fn()(2)
将会报错。
segmentfault 的@大笑平 补充的高颜值写法:
var curry = fn => judge = (...args) => args.length === fn.length ? fn(...args) : (arg) => judge(...args, arg)
参数不满足长度要求时,返回的函数仍需使用rest parameter:
(...arg) => judge(...args, ...arg)
,否则除了第一次可以传递多个参数,后面只能一个一个传到足够为止了,不能直接处理fn(1,2)(3,4,5)的情况。
function curry(length, fn) {
if (typeof length === 'function')
return curry.apply(this, Array.prototype.concat.apply([length.length], arguments));
var _args = arguments;
return function () {
var args = Array.prototype.slice.call(_args);
var i, next = 0, placeholderp = false;
while (++next < args.length && args[next] != curry._);
for (i = 0; i < arguments.length; i++) {
args[next] = arguments[i];
placeholderp |= args[next] === curry._;
while (++next < args.length && args[next] != curry._);
}
if (placeholderp || next < 2 + length || next < args.length)
return curry.apply(this, args);
else
return fn.apply(this, args.slice(2));
};
}
curry._ = {};
var _ = curry._;
var fn = curry(function (a, b, c, d, e) {
console.log([a, b, c, d, e]);
}, _, _, 3, 4, _);
fn(1, 2)(5);
fn(1, _)(2, _)(5);
// [1, 2, 3, 4, 5]
var arity3 = curry(3, function () {
console.log(arguments);
});
var arity2 = arity3(1);
var arity1 = arity3(1, 5) || arity2(5);
arity2(1, 2);
// [1, 1, 2];
arity1(2);
// [1, 5, 2]
var square = curry(Math.pow)(_, 2);
square(3);
// 9
var cubic = curry(Math.pow, _, 3);
cubic(2);
// 8
好文,可惜现在内力不足,还不是很能消化
我现在才知道 function 的 length 竟然就是参数的长度,太水了
很喜欢你的第二版,逻辑很清晰,很赞
其实表示的是未设置默认值的参数数量 比如
function test (arg1, arg2, arg3 = 3 ){
}
这里test.length=2
segmentfault 的@大笑平 补充的高颜值写法:
var curry = fn => judge = (...args) => args.length === fn.length ? fn(...args) : (arg) => judge(...args, arg)
最后 (arg) => judge(...args, arg)
, 修改为 (...arg) => judge(...args, ...arg)
,才进一步完善。
不然 fn("a")("b", "c")('d')
这种情况就会只接收了 a, b, d
三个参数而出现问题
rest可以这样用麽,记忆中是must be last
版本二写成这样不知可不可行?
function add(a, b, c, d) {
return a + b + c + d;
}
function curry(fn, length) {
return function (...args) {
if (args.length < length) {
return curry(fn.bind(this, ...args), length - args.length )
} else {
return fn.apply(this, args)
}
}
}
var addCurry = curry(add, 4);
console.log(addCurry(1)(2, 3, 4))
console.log(addCurry(1, 2, 3, 4)) // 10
@xiaohesong
rest
参数之后不能再有其他参数。
// 报错
function f(a, ...b, c) {
// ...
}
// 正确
function f(a, b, ...c) {
// ...
}
但调用函数时,...
是数组的扩展运算符 (spread) 。例如 fn(...[1, 2, 3, 4])
,它用来把一个数组转换为用逗号分隔的参数序列。
扩展运算符与正常的函数参数可以结合使用,例如 fn(0, ...[1, 2, 3, 4], 5)
@inottn 嗯是的,看错了。
我认为@大笑平老师的写法是非常赞的。 我在这里针对这个方法做了一个小的优化, 用于解决:curry4(1, 2)(3, 4)
const curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg)
柯里化的同时可以传入参数 const currying = (fn, ...arg1) => (...arg2) => [...arg1, ...arg2].length >= fn.length ? fn.apply(null, [...arg1, ...arg2]) : currying(fn, ...arg1, ...arg2)
我认为@大笑平老师的写法是非常赞的。 我在这里针对这个方法做了一个小的优化, 用于解决:curry4(1, 2)(3, 4)
const curry = fn => judge = (...args) => args.length === fn.length ? fn(...args) : (...arg) => judge(...args, ...arg)
这么写就不是柯里化了,柯里化一次传一个参数
segmentfault 的@大笑平 补充的高颜值写法:
var curry = fn => judge = (...args) => args.length === fn.length ? fn(...args) : (arg) => judge(...args, arg)
实践了一下,这里最后一行的参数只能在接受单个。应该写成:
let curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (...argMore) => judge(...args, ...argMore);
这样测试fn('a')('b','c')时才能跟之前一样
定义
维基百科中对柯里化 (Currying) 的定义为:
翻译成中文:
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
举个例子:
用途
我们会讲到如何写出这个 curry 函数,并且会将这个 curry 函数写的很强大,但是在编写之前,我们需要知道柯里化到底有什么用?
举个例子:
想想 jQuery 虽然有 $.ajax 这样通用的方法,但是也有 $.get 和 $.post 的语法糖。(当然 jQuery 底层是否是这样做的,我就没有研究了)。
curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。
可是即便如此,是不是依然感觉没什么用呢?
如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 map 呢?
举个例子:
比如我们有这样一段数据:
如果我们要获取所有的 name 值,我们可以这样做:
不过如果我们有 curry 函数:
我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?
但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了?
person.map(prop('name'))
就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。是不是感觉有点意思了呢?
第一版
未来我们会接触到更多有关柯里化的应用,不过那是未来的事情了,现在我们该编写这个 curry 函数了。
一个经常会看到的 curry 函数的实现为:
我们可以这样使用:
已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。
第二版
我们验证下这个函数:
效果已经达到我们的预期,然而这个 curry 函数的实现好难理解呐……
为了让大家更好的理解这个 curry 函数,我给大家写个极简版的代码:
大家先从理解这个 curry 函数开始。
当执行 fn1() 时,函数返回:
当执行 fn1()() 时,函数返回:
当执行 fn1()()() 时,函数返回:
当执行 fn1()()()() 时,因为此时 length > 2 为 false,所以执行 fn():
再回到真正的 curry 函数,我们以下面的例子为例:
当执行 fn1("a", "b") 时:
当执行 fn1("a", "b")("c") 时,函数返回:
当执行 fn1("a", "b")("c")("d") 时,此时 arguments.length < length 为 false ,执行 fn(arguments),相当于:
函数执行结束。
所以,其实整段代码又很好理解:
sub_curry 的作用就是用函数包裹原函数,然后给原函数传入之前的参数,当执行 fn0(...)(...) 的时候,执行包裹函数,返回原函数,然后再调用 sub_curry 再包裹原函数,然后将新的参数混合旧的参数再传入原函数,直到函数参数的数目达到要求为止。
如果要明白 curry 函数的运行原理,大家还是要动手写一遍,尝试着分析执行步骤。
更易懂的实现
当然了,如果你觉得还是无法理解,你可以选择下面这种实现方式,可以实现同样的效果:
或许大家觉得这种方式更好理解,又能实现一样的效果,为什么不直接就讲这种呢?
因为想给大家介绍各种实现的方法嘛,不能因为难以理解就不给大家介绍呐~
第三版
curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢?
我们可以创建一个占位符,比如这样:
我们直接看第三版的代码:
写在最后
至此,我们已经实现了一个强大的 curry 函数,可是这个 curry 函数符合柯里化的定义吗?柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,可是什么又是偏函数呢?下篇文章会讲到。
专题系列
JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。