介绍
函数式有不少处理输入参数的工具方法
- identity
- function identity(v) {
- return v
- }
- unary
- function unary(fn) {
- return function onlyOneArg(arg) {
- return fn(arg)
- }
- }
- spreadArgs
- function spreadArgs(fn) {
- return function spreadFn(argsArr){
- return fn(...argsArr)
- }
- }
- gatherArgs
- function gatherArgs(fn) {
- return function gatheredFn(...argsArr) {
- return fn(...argsArr)
- }
- }
- reverseArgs
- function reverseArgs(fn) {
- return function argsReversed(...args) {
- return fn(...args.reverse())
- }
- }
- ...
- ...
一切都很简单, 确实, 有的函数 (如 identity) 简单到你可能都不知道它能用来做什么
函数式里的函数就像积木一样, 每块积木看上去都是这么简单
至于怎么 "堆" 积木, 可以用 compose 来组合他们, 以后再关注这些方法的具体使用
这次主要是来学习一下, partial 和 curry 这两个对输入参数处理的方法, 其实它们是 js 本身就有的功能, 只不过函数式更多使用它们作为工具.
partial
举个例子, 你有一个 ajax 函数
- function ajax(url, data, callback) {
- // ...
- }
你事先知道 url, 但是 data, callback 可能要等一会 (比如等用户输入完表单) 才知道
(当然你可以等输入参数都 ok, 再调用)
这里可以创建一个新函数, 内部调用 ajax, 并传入 url, 等待 data, callback 参数
- function getPerson(data,cb) {
- ajax( "http://some.api/person", data, cb );
- }
- function getOrder(data,cb) {
- ajax( "http://some.api/order", data, cb );
- }
很快, 手动操作, 就会变得很无趣, 特别是如果已知参数变化, 比如我们不仅事先知道 url 还知道 data
- function getCurrentUser(cb) {
- getPerson( { user: CURRENT_USER_ID }, cb );
- }
这时候我们需要寻找一个较为通用的工具方法 仔细观察发现, 我们将部分实参预先应用到形参, 而将剩余的实参推迟应用
partial(部分应用) -- 其可以减少函数的输入参数的个数
- // sq:
- function partial(fn, ...presetArgs) {
- return function partiallyApplied(...laterArgs) {
- return fn(...presetArgs, ...laterArgs)
- }
- }
使用
- const getPerson = partial( ajax, "http://some.api/person" )
- const getOrder = partial( ajax, "http://some.api/order" )
- // 当然你可以写成 partial(ajax, 'http://xxx' { user: CURRENT_USRE_ID })
- // 但下面这种更合适一些
- const getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } )
- // 为方便理解, 展开一下 getCurrentUser
- var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs){
- var getPerson = function innerPartiallyApplied(...innerLaterArgs){
- return ajax("http://some.api/person", ...innerLaterArgs);
- }
- return getPerson({ user: CURRENT_USER_ID }, ...outerLaterArgs)
- }
例 2
- function add(x, y) {
- return x + y
- }
- // [11, 12, 13]
- [1, 2, 3].map(function adder(val) {
- return add(val + 10)
- })
- // 改用 partial 来将 add 函数适配 map 回调函数
- [1, 2, 3].map(partial(add, 10))
- partialRight
如果上面的函数, 我们预先知道的是 data 和 callback, 而暂时不知道 url 呢?
版本一, 使用前面的 reverseArgs(反转参数)及 partial
- function partialRight(fn,...presetArgs) {
- return reverseArgs(
- partial( reverseArgs( fn ), ...presetArgs.reverse() )
- )
- }
- // 使用
- function add(a, b, c, d) {
- return a + b * 2 + c * 3 + d * 4
- }
- const add2 = partialRight(add, 30, 40)
- // 10 + 20 * 2 + 30 * 3 + 40 * 4 = 300
- add2(10, 20)
- // 理解 partialRight
- // reverseArgs(fn)返回一个函数, 调用 fn 时将参数反转
- let fn2 = function argsReversed(...args) {
- return fn(...args.reverse())
- }
- // partial(reverseArgs(fn), ...presetArgs.reverse())的返回函数
- // 试想一下, 调用 p, 会调用 fn(...laterArgs.reverse(), ...presetArgs)
- // 所以需要对 laterArgs 也反转一次参数, fn(...lasterArgs, ...presetArgs)
- let p = function partiallyApplied(...laterArgs) {
- return fn2(...presetArgs.reverse(), ...laterArgs)
- }
建议还是敲一下代码, 或者在纸上写一下
版本二 实际上, 版本一可以当个练习, 帮助理解函数式, 其实可以更直接
- // sq:
- function partialRight(fn, ...presetArgs) {
- return function partiallyApplied(...laterArgs) {
- return fn(...laterArgs, ...presetArgs)
- }
- }
- curry
curry 即柯里化, 将一个接收多个参数的函数分解一个连续的链式函数, 其中每个函数接收一个参数而返回另一个函数接收下一个参数.(宽松型 curry 每个函数可以接收多个参数)
上例 ajax, 如果使用 curry
- var personFetcher = curriedAjax( "http://some.api/person" )
- var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } )
- getCurrentUser( function foundUser(user){ /* .. */ } );
curry 和 partial 很相似, 但 curry 返回的函数, 只能接收下一个参数
- // sq:
- function curry(fn, arity = fn.length) {
- return (function nextCurried(prevArgs) {
- return function curried(nextArg) {
- var args = [...prevArgs, nextArg]
- if (args.length>= arity) {
- return fn(...args)
- } else {
- return nextCurried(args)
- }
- }
- })([])
- }
注意: 函数默认值形式, 析构, 展开运算符形式... 会导致 fn.length 不正确, 所以此时应传入函数正确接收参数个数
之前的例 2, 使用 curry
- function add(x, y) {
- return x + y
- }
- var adder = curry( add )
- [1, 2, 3].map(adder(10))
例 3
- function sum(...nums) {
- var total = 0;
- for (let num of nums) {
- total += num;
- }
- return total;
- }
- // 15
- sum(1, 2, 3, 4, 5)
- var curriedSum = curry(sum, 5)
- // 15
- curriedSum(1)(2)(3)(4)(5)
实际上使用 partial 也是可以做到只接收一个参数, 只需要一直对部分应用的函数手动连续调用 partial, 而 curry 则是自动.
- function add(a, b, c) {
- return a + b + c
- }
- const partial1 = partial(add, 1)
- const partial2 = partial(partial1, 2)
- const partial3 = partial(partial2, 3)
- // 6
- partial3()
参数传入次数太多有时也挺麻烦, 所以也允许 curry 传入多个参数, 大部分库也是这样做的
- // sq:
- function looseCurry(fn, arity = fn.length) {
- return (function nextCurried(prevArgs) {
- return function curried(...nextArgs) {
- var args = [...prevArgs, ...nextArgs];
- if (args.length>= arity) {
- return fn(...args);
- } else {
- return nextCurried(args);
- }
- };
- })([]);
- }
当然 curry 也有 curryRight, 暂时不过多介绍, 以后用到再写
小结
库的真实实现可能略有区别, 因为它毕竟要考虑更多方法的通用作出更高层次的抽象, 但是上面的代码已经很好的可以帮助我们理解 partial 和 curry, 从而窥见函数式的冰山一角
至于代码风格, 你要是更喜欢 ES6 的箭头函数, 完全可以依据喜好, 当然命名函数是有诸多好处的, 比如可读性, 调试等
参考
Functional Programming in JavaScript https://www.manning.com/books/functional-programming-in-javascript
- https://legacy.gitbook.com/book/jigsawye/mostly-adequate-guide/details
- https://github.com/getify/Functional-Light-JS/
- https://github.com/stoeffel/awesome-fp-js
来源: https://juejin.im/post/5b3db0836fb9a04f951cf70d