1. 什么是函数式编程及其好处
函数式编程是一种编码思想,是一种通过编写纯函数、无副作用、不改变外部状态的一种编码构建方式。
函数式编程是声明式的不是命令式的。声明式编程是将程序的描述和求值分离开来的。更多的是关注于表达式的程序逻辑并将控制流交给其他部分去处理。
函数式编程的火热和受追捧是其具有易扩展性、易重构性、易测试性、复用性强等优点。
在刚接触函数式编程时被其多个术语吓到,脑袋里完全是“这是在说啥?彷如我是一个智障 …”, 但多阅读和仔细看几遍例子会发现平时还是有接触过,只不过现在都披了一层外衣。
2. 函数式编程的基本名词解释
2.1 纯函数
即一个相同的输入只有相同的结果,并且不会产生副作用。
相反不纯的函数在相同的输入是会出现不同的结果的。
const array = [1, 2, 3]
// 纯函数
array.slice(0, 3) // [1, 2, 3]
array.slice(0, 3) // [1, 2, 3]
// 不纯的函数
numbers.splice(0, 3) // [1, 2, 3]
numbers.splice(0, 3) // []
2.2 副作用
副作用就是函数在运用的过程中改变外部变量的状态或与函数外的状态进行交互。纯的且无副作用的函数在实际编码中是很难使用的,前端编程中几乎时时刻刻与用户进行交互也存在改变外部的状态。我们只能尽可能的减少函数的副作用,保持函数的纯度。
2.3 引用透明
引用透明是指函数中的变量都是来源于函数参数的,不会引入任何非参数传递形式的变量。这样可以明确每次数据的来源,减少函数的副作用并保证函数的纯度。
2.4 函数是 ” 一等公民 ”
是指函数跟其他变量一样具有相同的行为,可以是一个函数的参数、函数的返回值、进行赋值等操作。
另外这个 ” 一等公民的特性 ” 我们经常用到。比如:将函数赋值给一个变量、异步操作式的回调函数、闭包返回的函数等
2.5 函数的柯里化
咋一眼看“柯里化”这个词,以为是把函数分成多个的小小的细细的的小函数进行复用。简单的说就是在函数调用时如果只传部分参数则返回一个可接收剩余参数的函数,待所有参数全部传完返回最终结果。
// 如果未传递所有参数,返回一个函数,等待剩余的参数。const curry = fn => firstArg => secondArg => fn(firstArg,secondArg)
const add = (a,b)=>a+b;
const sum = curry(add)
sum(1000)(24)
//1024
通过拆分函数参数将函数变为了高阶函数。于此同时想要是有 n 个参数难道要一层层的套进去吗?心想偶买噶,这比剥洋葱还辛苦。直到看到某个例子时,骤然感到”too young too simple“(详见 3.1)
2.6 高阶函数
作为其他函数的参数或别其他函数返回的函数。
3. 函数式编程的核心内容
3.1 函数的”柯里化“
由于 javascript 的特殊性在函数 f(a,b,c) 调用时, 仅传 a 的值剩余的参数会被设为 undefined。而柯里化函数,它要求所有参数都有明确的定义,当使用部分参数调用时,会返回一个新的函数接收剩余参数,当剩余参数被提供后调用返回最终结果。
在实际开发中,有很多函数都不是柯里化的,可以使用函数转化,也可以用 loash 的 curry 函数
/* 当未传全部参数,返回接收剩余参数的函数。* 参数全部传入,执行函数
* 这里最神奇的是 args 里面记录的是全部传入的参数。* 是通过闭包特殊性将 args 的一次次传入的变量存储在内存
*/
const curry = (fn)=>{
const arity = fn.length
return function $curry(...args){if(args.length < arity){return $curry.bind(null, ...args)
}
return fn.call(null, ...args)
}
}
const curryJoin = curry((tag, str)=>str.join(tag))
const joinCom = curryJoin('-') // 返回一个函数
joinCom(["h", "e", "l", "l", "o"]) // "h-e-l-l-o"
3.2 函数组合
将已经分解的简单函数组合成复杂行为的过程。也就是说有函数 f(x),g(x)。通过组合变成 f(g(x)) 的过程。类似于 f(x)*g(x)=>f(g(x)), 相当于编程中 g(x) 的结果作为 f(x) 的参数。
肯定有人想既然这样,为啥还有组合,我直接把函数当参数传递不就好了,这样做实现功能当然没问题。但不易于维护呀,如果我要重构 f(x) 不是还得看 g(x) 这个函数的逻辑吗?使得重构流程变长难以维护。函数组合的函数在于描述和求值分开。
const compose = (f,g)=> x => f(g(x))
const split = curry((tag,x)=> x.split(tag))
const reverse = x=> x.reverse()
const join = curry((tag,x)=> x.join(tag))
const composeName = compose(reverse, split('-'))
beginCompose('cheery-zhang') // ['zhang','cherry']
const replaceComponse = compose(join('love'),split('-'))
replaceComponse('l-china') // "l love china"
可以看到有了组合之后,将求值的每一步分解成一个函数,再通过需求将不同的函数进行组合。这样重构时只用关心分解后的函数。使维护变得简单些。
3.3 函子 (Functor)
将一个集合通过函子转为另外一个集合。首先需要容器内可以装载数据,容器通过 map 方法让外部函数可以操控容器内的值。定义一个 Container 容器,为了避免每次 new Container 则添加 of 方法返回新创建的 Container。并暴露一个 map 方法使得外部函数可以操作容器中的值, 还能连续调用 map 进行多次操作。
// 下面来操作一下容器的值
const Container = x=> this._value = x
Container.of = x=> new Container(x)
Container.prototype.map = f=> Container.of(f(this._value))
Container.of(19).map(x=> x-1)// Container(18)
Container.of('cherry').map(concat('very nice'))//Container('cherry very nice')