昨天翻译的文章中,原作者对于柯里化方法的最终实现是这样:
// 定义占位符
var _ = '_';
function magician3 (targetfn, ...preset) {
var numOfArgs = targetfn.length;
var nextPos = 0; // 下一个有效输入位置的索引,可以是'_',也可以是preset的结尾
// 查看是否有足够的有效参数
if (preset.filter(arg=> arg !== _).length === numOfArgs) {
return targetfn.apply(null, preset);
} else {
// 返回'helper'函数
return function (...added) {
// 循环并将added参数添加到preset参数
while(added.length > 0) {
var a = added.shift();
// 获取下一个占位符的位置,可以是'_'也可以是preset的末尾
while (preset[nextPos] !== _ && nextPos < preset.length) {
nextPos++
}
// 更新preset
preset[nextPos] = a;
nextPos++;
}
// 绑定更新后的preset
return magician3.call(null, targetfn, ...preset);
}
}
}
这个实现存在一点问题:
var abc = function(a, b, c) { return [a, b, c];};
var curried = magician3(abc);
curried(1)(2)(3) // [1, 2, 3]
curried(1)('_', 2)(3) // Uncaught TypeError: curried(...)(...) is not a function
curried 第一次运行功能正常,但是第二次运行就报错了。而且这个错误和使用占位符没有关系,第二次执行换作 curried(2)(3)(4) 也还是会报同样的错误。
一时看不出错误出在哪里,我先理一下上面的代码在运行时到底发生了什么:
-
初始化变量
_,值为'_'。 -
初始化变量
abc,指向一个函数 ——function(a, b, c) { return [a, b, c];},该函数用来将传入的参数转化为数组并返回。 -
初始化变量
curried,实际上获得的是magician3(abc)执行之后的结果。那接着看magician3(abc)的执行过程:-
targetfn初始化为传进来的abc,参数preset的位置什么也没传,那么,preset就被初始化为了一个空数组[]。 -
numOfArgs初始化为targetfn.length,即3。 -
nextPos初始化为0。 -
进入条件判断语句,判断条件
preset.filter(arg=> arg !== _).length === numOfArgs实际上是在说:检查一下数组preset中不为_的元素数量是不是等于numOfArgs,也就是targetfn.length,目标函数所期望的参数数量。这里没有传preset,显然不符合判断条件,进入else。 -
else里面return出去了一个function。这时,magician3(abc)就执行完了,那curried拿到的就是return出来的这个function的引用。这个function通过闭包引用了一个空数组preset,以及一个numOfArgs,即所期望的参数数量 ——3,一个nextPos——0,还有一个targetfn,即abc。
-
-
代码继续运行,到了
curried(1)(2)(3)这句。这句代码会先执行curried(1),再调用curried(1)返回的结果并传入2,继续调用返回的结果并传入3。 -
curried(1)执行,代码进入curried所引用的函数,也就是之前magician3(abc)执行完之后返回的匿名函数,该函数内,added被初始化为[1]。 -
继续执行,到了
while循环。这个循环内部还有一个while循环,先看里面这个循环。
这个循环的终止条件是 preset[nextPos] !== _ && nextPos < preset.length,其实就是在说:帮我检查一下 preset 数组 nextPos 位置的元素是不是不等于 '_' ,并且 nextPos 要小于 preset 的长度,不满足这个条件的话 nextPos 就一直累加。
那能看出来这个 while 循环其实就是在数组 preset 中找一个位置,这个位置要么是 '_' ,要么是数组的末尾,一旦找到,循环就终止。
结合这些再来看外层的 while 循环,这个循环的判断条件是 added.length > 0,循环体内首先执行 added.shift() 的操作,并将结果给到变量 a,其实就是 added 中的第一个元素,然后是用来找位置(nextPos)的循环,找到之后把 a 放到 preset 的 nextPos 位置上,放完之后再把 nextPos 向后移一位,也就是 nextPos++,以便需要时继续往下找。
那可以看出,这个大的 while 循环作用其实就是将 added 中的元素从头到尾按顺序逐个给到 preset,并放到正确的位置上。什么是正确的位置呢,就是此时 added 里的元素逐个地,要么放到 preset 中 '_' 的位置(替换),如果没有 '_' 的话,就放到 preset 末尾(追加)。
那么,经过了外层这段 while 循环之后,preset 就变成了 [1],而 added 变成了 []。
-
代码继续执行,到了
return magician3.call(null, targetfn, ...preset);。这句又执行了magician3函数,并将targetfn和...preset(也就是1)作为magician3执行时传递的参数。那接着看具体都发生了什么:-
进入
magician3方法,targetfn还是那个targetfn,指向abc,preset已经变成了[1]。 -
numOfArgs初始化为targetfn.length,即3。 -
nextPos初始化为0。 -
进入条件判断语句,这时
preset里只有1一个元素,显然还是会进入else。 -
else里还是return出去了一个function,但这时这个function通过闭包引用到的preset已经是[1]了。
-
-
继续执行,
curried(1)(2)(3),这次调用curried(1)执行完返回的函数时传入的参数就是2了。 -
那么同样地,进入上一步返回的匿名函数,
while循环执行完之后,preset变成了[1, 2]。 -
继续
return magician3.call(null, targetfn, ...preset);,同样执行magician3函数,这次传入的preset为[1, 2]。继续看magician3的执行过程:-
进入
magician3方法,targetfn还是abc,preset变成了[1, 2]。 -
numOfArgs初始化为targetfn.length,即3。 -
nextPos初始化为0。 -
进入条件判断语句,这时
preset里只有1和2两个元素,还是会进入else。 -
else里还是return出去一个function,这时通过闭包引用到的preset是[1, 2]。
-
-
继续执行,
curried(1)(2)(3),这次调用返回的函数时传入的参数是3。 -
同样地,进入返回的匿名函数,
while循环执行完之后,preset变为[1, 2, 3]。 -
继续
return magician3.call(null, targetfn, ...preset);,同样执行magician3函数,这次传入的preset变成了[1, 2, 3]。继续看magician3的执行过程:-
进入
magician3方法,targetfn还是指向abc,preset变成了[1, 2, 3]。 -
numOfArgs初始化为targetfn.length,即3。 -
nextPos初始化为0。 -
进入条件判断语句,这次
preset是[1, 2, 3],满足判断条件preset.filter(arg=> arg !== _).length === numOfArgs,那么进入if语句块,return targetfn.apply(null, preset);,也就是执行targetfn,并传入preset,使用apply时可以传入参数数组,其实就相当于targetfn(1, 2, 3),也就是abc(1, 2, 3),这时进入到function(a, b, c) { return [a, b, c];},返回了结果[1, 2, 3]。
-
一直到这里看起来都是正常的,但是,再执行 curried(1)(_, 2)(3) 的话就出现了问题。我在想,curried 已经是指向一个匿名函数的引用了,那第一次执行这个函数没有问题,第二次执行时怎么就有问题了呢?
想了很久没想清楚这个 bug 的来龙去脉,后来在春岩和唐老师的指导下,逐渐明白了其中道理。
curried(1)(2)(3) 执行时,先执行 curried(1)。
执行 curried(1) 之前, curried 指向的是 magician3(abc) 执行完之后 return 出来的那个匿名函数,该匿名函数通过闭包引用着 preset —— 一个空数组 []。
那等 curried(1) 执行时,该匿名函数通过闭包引用的 preset 就变成了 [1](我把这个 preset 称为初始 preset),最后 return magician3.call(null, targetfn, ...preset); ,执行 magician3 并传入解构之后的 preset。
代码接着走,又返回了另外一个匿名函数,这个匿名函数和之前 curried 持有的那个匿名函数之间互不影响,唯一的联系是,curried 持有的匿名函数所引用的 preset 浅复制之后的值被后来返回的这个匿名函数引用着。
后来的这个匿名函数执行时传入了 2,那么这个匿名函数执行完之后 preset 就变成了 [1, 2],但是这个 preset 和“初始 preset”是“存在不同地方的”,所以两者之间互不影响。
继续调用后来返回的这个匿名函数并传入 3 时,同样的,preset变成了 [1, 2, 3],但是这个 preset 又是存在另外一个地方的 preset。
所以直到 curried(1)(2)(3) 执行完,curried 指向的匿名函数通过闭包引用的 preset 还是那个被 curried(1) 改变后的 preset,也就是 [1]。
所以,问题就在这里。
preset 中已经有一个元素了,这会导致 curried(1)(_, 2)(3) 在还没有执行完时(执行了 curried(1)(_, 2) )就返回了一个数组,返回数组之后调用该数组并传入参数 3,显然数组是不能当作函数执行的,所以就报出了 Uncaught TypeError: curried(...)(...) is not a function 的错误。
curried(1)(_, 2)(3) 执行时,preset 应该是空数组才对。
那么可以这么改:
var _ = '_';
function magician3 (targetfn, preset = []) {
var numOfArgs = targetfn.length;
if (preset.filter(arg=> arg !== _).length === numOfArgs) {
return targetfn.apply(null, preset);
} else {
return function (...added) {
var newPreset = [...preset]
var nextPos = 0;
while(added.length > 0) {
var a = added.shift();
while (newPreset[nextPos] !== _ && nextPos < newPreset.length) {
nextPos++
}
newPreset[nextPos] = a;
nextPos++;
}
return magician3.call(null, targetfn, newPreset);
}
}
}
var abc = function(a, b, c) { return [a, b, c];};
var curried = magician3(abc);
curried(1)(2)(3) // [1, 2, 3]
curried(1)('_', 2)(3) // [1, 3, 2]
把 preset 浅复制一份后给 newPreset,然后把 newPreset 和 nextPos 都放在返回的匿名函数内部,每个返回的匿名函数都维护着自己的数据,各自间彻底互不影响,不再像修改之前一样通过闭包引用保存 preset 和 nextPos。
最后在匿名函数内部把 newPreset 传给 magician3,magician3 用参数 preset 来接收 newPreset,如果没传的话( magician3(abc) ),默认值为空数组 []。
这样,问题就解决了。
回头再看修改之前的代码,这时会觉得 magician3 返回的匿名函数不是一个好函数。因为好函数不应该有副作用,而且应该是幂等的。好函数任意多次执行所产生的影响均与一次执行的影响相同。好函数可以使用相同参数重复执行,并能获得相同的执行结果。也就是说,固定输入,固定输出。很显然,它是一个坏函数。
唐老师看到这个坏函数时说,写出这样的代码不如回家养猪。可就是这样的代码,春岩讲得姿仪万方,我却听得跌跌撞撞。唉,我真的好菜...










网友评论