这篇文章简单实现了curry函数,主要是为了巩固实现过程中用到的js知识,并且更深刻的了解函数柯里化的概念。
函数柯里化是和函数式编程密切相关的一个概念,可以先读下面的这篇文章,
张鑫旭–js中的柯里化,初步的理解这个概念。
概括来讲,函数柯里化的作用和特点就是参数复用,提前返回,延迟执行。下面通过代码来初探函数柯里化的实现。
预备阶段
首先需要接受或者习惯一个js的基础知识,因为平时代码中用的不多,所以刚开始看到代码还会有点陌生和不知所云。其实就是基础知识罢了。
下面用es5和es6两种写法来写,方便理解,在比较中也可以更好的学习es6。
1 2 3 4 5
| function add(a) { return function(b) { return a + b } }
|
1
| const add = a => b => a + b
|
我们定义了一个求和函数,但是,调用方法有些特殊:add(1)(2); // 3
。
如果我们分开来调用,就可以看得更加明显:
1 2
| const add1 = add(1); // 这里就体现了参数复用的特点 add1(2); // 3
|
总结下:
- 将一般的求和函数
const sum = (a, b, c) => a + b + c
转化为const add = a => b => c => a + b + c
的过程,就叫做函数的柯里化过程。转化而来的函数就是柯里化函数。
fn(a)(b)(c)
这种调用函数的方法,意味着fn接受一个参数a,然后返回了一个函数接受参数b,又返回了一个函数接受参数c……最后,将所有的参数进行处理。
初步实现
下面我们尝试实现一个curry函数,它的作用就是将一个函数柯里化,即curry(sum)(a)(b)(c) <==> add(a)(b)(c)
通过上面的分析,可以有个初步的思路,我们先定义一个数组,将函数的参数依次收集起来,然后将这些参数加起来.
1 2 3 4 5 6 7 8 9 10 11
| function curry(fn) { var arr = []; return function curring() { var arg = [].slice.call(arguments); arr = arr.concat(arg); if (arr.legnth < fn.length) { return curring; } return fn.apply(this, arr); } }
|
1 2 3 4 5 6 7 8
| const curry = fn => { let arr = []; const curring = (...arg) => { arr = arr.concat(arg); return arr.length < fn.length ? curring : fn.apply(this, arr); } return curring }
|
上面的代码用到了闭包保存我们收集参数的数组,通过递归来依次收集参数(这里用到了function的length属性,这个属性会返回一个函数预期传入的参数个数,也就是形式参数的个数)。
如果还是不理解,可以在浏览器里分步执行,比如curry(sum)(1)(2)(3)
,curry一共执行了三次,arg分别是[1], [2], [3],arr分别是[], [1], [1, 2];另外,函数中的this一直指向的window。
补充一下,这个柯里化函数还可以每次传入不定的参数进行调用,如curry(sum)(1, 2)(3)
或者curry(sum)(1, 2, 3)
另一个版本
不多废话了,直接看代码
1 2 3 4 5
| const curry = (fn, arr = []) => ( (...arg) => ( a => a.length === fn.length ? fn(...a) : curry(fn, a) )([...arr, ...arg]) )
|
因为最初看到的这个版本就是es6的,感觉比上面的更加简练一点,当然也更加装逼。。。仔细分析一下可以看到,这个版本通过一个默认参数和一个立即执行函数,避免了定义收集参数的数组arr,通过灵活使用扩展运算符,替代了arguments对象也完成了数组的拼接。
下面试着用es5的方法改写一下,加深理解
1 2 3 4 5 6 7 8 9
| function curry(fn, arr) { var arr = (arr !== undefined && arr !== null) ? arr : []; return function() { var arg = [].slice.call(arguments) return (function(a) { return a.length === fn.length ? fn.apply(this, a) : curry(fn, a) })(arr.concat(arg)) } }
|
上面是我改写的版本,有个地方明显是没改好,就是最后[...arr, ...arg]
这里,我直接改写为concat了。由于扩展运算符也可以用于处理字符串,和类数组,所以直接改成concat不是很合理。但是理解这个函数的思路是够用了。
下面是我在babel官网进行转译的结果,看看babel是怎么处理扩展运算符的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } var curry = function curry(fn) { var arr = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; return function () { for (var _len = arguments.length, arg = Array(_len), _key = 0; _key < _len; _key++) { arg[_key] = arguments[_key]; } return (function (a) { return a.length === fn.length ? fn.apply(undefined, _toConsumableArray(a)) : curry(fn, a); })([].concat(_toConsumableArray(arr), arg)); }; };
|
从上面一系列的转化分析中可以看出,这个版本的curry函数可以接受第二个参数,这个参数必须能被扩展运算符操作,否则就会报错,比如一个数字。不过单纯实现我们的需求,这个版本是相对比较简单的。
总结
这篇文章涉及到的js知识有:
- 函数柯里化
- 闭包
- 函数的length属性
- 函数的call和apply方法
- 函数的arguments对象
- es6的箭头函数和扩展运算符
- 函数的递归
- 立即执行函数
what’s more
一些javascript库早就提供了curry方法,比如大名鼎鼎的lodash。lodash的curry函数就比上面的版本更加健壮,当然其源码也更加复杂。
下面是lodash的curry方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| /** * Creates a function that accepts arguments of `func` and either invokes * `func` returning its result, if at least `arity` number of arguments have * been provided, or returns a function that accepts the remaining `func` * arguments, and so on. The arity of `func` may be specified if `func.length` * is not sufficient. * * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, * may be used as a placeholder for provided arguments. * * **Note:** This method doesn't set the "length" property of curried functions. * * @static * @memberOf _ * @since 2.0.0 * @category Function * @param {Function} func The function to curry. * @param {number} [arity=func.length] The arity of `func`. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Function} Returns the new curried function. * @example * * var abc = function(a, b, c) { * return [a, b, c]; * }; * * var curried = _.curry(abc); * * curried(1)(2)(3); * // => [1, 2, 3] * * curried(1, 2)(3); * // => [1, 2, 3] * * curried(1, 2, 3); * // => [1, 2, 3] * * // Curried with placeholders. * curried(1)(_, 3)(2); * // => [1, 2, 3] */ function curry(func, arity, guard) { arity = guard ? undefined : arity; var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); result.placeholder = curry.placeholder; return result; }
|