一个持续更新的github笔记,连接地址:Front-End-Basics,能够watch,也能够star。javascript
此篇文章的地址:JavaScript函数式编程入门经典html
正文开始java
f(x) = y
// 一个函数f,以x为参数,并返回输出y
复制代码
关键点:git
函数式编程技术主要基于数学函数和它的思想,因此要理解函数式编程,先了解数学函数是有必要的。github
函数是一段能够经过其名称被调用的代码。它能够接受参数,并返回值。面试
与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)同样,函数式编程(Functional programming)也是一种编程范式。咱们可以以此建立仅依赖输入就能够完成自身逻辑的函数。这保证了当函数被屡次调用时仍然返回相同的结果(引用透明性)。函数不会改变任何外部环境的变量,这将产生可缓存的,可测试的代码库。编程
全部的函数对于相同的输入都将返回相同的值,函数的这一属性被称为引用透明性(Referential Transparency)数组
// 引用透明的例子,函数identity不管输入什么,都会原封不动的返回
var identity = (i) => {return i}
复制代码
把一个引用透明的函数用于其余函数调用之间。缓存
sum(4,5) + identity(1)
bash
根据引用透明的定义,咱们能够把上面的语句换成:
sum(4,5) + 1
该过程被称为替换模型(Substitution Model),由于函数的逻辑不依赖其余全局变量,你能够直接替换函数的结果,这与它的值是同样的。因此,这使得并发代码和缓存成为可能。
并发代码: 并发运行的时候,若是依赖了全局数据,要保证数据一致,必须同步,并且必要时须要锁机制。遵循引用透明的函数只依赖参数的输入,因此能够自由的运行。
缓存: 因为函数会为给定的输入返回相同的值,实际上咱们就能缓存它了。好比实现一个计算给定数值的阶乘的函数,咱们就能够把每次阶乘的结果缓存下来,下一次直接用,就不用计算了。好比第一次输入5,结果是120,第二次输入5,咱们知道结果必然是120,因此就能够返回已缓存的值,而没必要再计算一次。
函数式编程主张声明式编程和编写抽象的代码。
// 有一个数组,要遍历它并把它打印到控制台
/*命令式*/
var array = [1,2,3]
for(var i = 0; i < array.length; i++)
console(array[i]) // 打印 1,2,3
// 命令式编程中,咱们精确的告诉程序应该“如何”作:获取数组的长度,经过数组的长度循环数组,在每一次循环中用索引获取每个数组元素,而后打印出来。
// 可是咱们的任务只是打印出数组的元素。并非要告诉编译器要如何实现一个遍历。
/*声明式*/
var array = [1,2,3]
array.forEach((element) => console.log(element)) // 打印 1,2,3
// 咱们使用了一个处理“如何”作的抽象函数,而后咱们就能只关心作“什么”了
复制代码
大多数函数式编程的好处来自于编写纯函数,纯函数是对给定的输入返回相同的输出的函数,而且纯函数不该依赖任何外部变量,也不该改变任何外部变量。
程序做用于数据,数据对于程序的执行很重要。每种编程语言都有数据类型。这些数据类型可以存储数据并容许程序做用其中。
**当一门语言容许函数做为任何其余数据类型使用时,函数被称为一等公民。**也就是说函数可被赋值给变量,做为参数传递,也可被其余函数返回。
函数做为JavaScript的一种数据类型,因为函数是相似String的数据类型,因此咱们能把函数存入一个变量,可以做为函数的参数进行传递。因此JavaScript中函数是一等公民。
接受另外一个函数做为其参数的函数称为高阶函数(Higher-Order-Function),或者说高阶函数是接受函数做为参数而且/或者返回函数做为输出的函数。
通常而言,高阶函数一般用于抽象通用的问题,换句话说,高阶函数就是定义抽象。
抽象 : 在软件工程和计算机科学中,抽象是一种管理计算机系统复杂性的技术。 经过创建一我的与系统进行交互的复杂程度,把更复杂的细节抑制在当前水平之下。简言之,抽象让咱们专一于预约的目标而无须关心底层的系统概念。
例如:你在编写一个涉及数值操做的代码,你不会对底层硬件的数字表现方式究竟是16位仍是32位整数有很深的了解,包括这些细节在哪里屏蔽。由于它们被抽象出来了,只留下了简单的数字给咱们使用。
// 用forEach抽象出遍历数组的操做
const forEach = (array,fn) => {
let i;
for(i=0;i<array.length;i++) {
fn(array[i])
}
}
// 用户不须要理解forEach是如何实现遍历的,如此问题就被抽象出来了。
//例如,想要打印出数组的每一项
let array = [1,2,3]
forEach(array,(data) => console.log(data))
复制代码
什么是闭包?简言之,**闭包就是一个内部函数。**什么是内部函数?就是在另外一个函数内部的函数。
闭包的强大之处在于它对做用域链(或做用域层级)的访问。从技术上讲,闭包有3个可访问的做用域。
(1) 在它自身声明以内声明的变量
(2) 对全局变量的访问
(3) 对外部函数变量的访问(关键点)
实例一:假设你再遍历一个来自服务器的数组,并发现数据错了。你想调试一下,看看数组里面究竟包含了什么。不要用命令式的方法,要用函数式的方法来实现。这里就须要一个 tap 函数。
const tap = (value) => {
return (fn) => {
typeof fn === 'function' && fn(value)
console.log(value)
}
}
// 没有调试以前
forEach(array, data => {
console.log(data + data)
})
// 在 forEach 中使用 tap 调试
forEach(array, data => {
tap(data)(() => {
console.log(data + data)
})
})
复制代码
完成一个简单的reduce函数
const reduce = (array,fn,initialValue) => {
let accumulator;
if(initialValue != undefined)
accumulator = initialValue
else
accumulator = array[0]
if(initialValue === undefined)
for(let i = 1; i < array.length; i++)
accumulator = fn(accumulator, array[i])
else
for(let value of array)
accumulator = fn(accumulator,value)
return accumulator
}
console.log(reduce([1,2,3], (accumulator,value) => accumulator + value))
// 打印出6
复制代码
只接受一个参数的函数称为一元(unary)函数。
只接受两个参数的函数称为二元(binary)函数。
变参函数是接受可变数量的函数。
柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程。
例如
// 一个多参数函数
const add = (x,y) => x + y;
add(2,3)
// 一个嵌套的一元函数
const addCurried = x => y => x + y;
addCurried(2)(3)
// 而后咱们写一个高阶函数,把 add 转换成 addCurried 的形式。
const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg,secondArg)
}
}
}
let autoCurriedAdd = carry(add)
autoCurriedAdd(2)(3)
复制代码
上面只是简单实现了一个二元函数的柯里化,下面咱们要实现一个更多参数的函数的柯里化。
const curry = (fn) => {
if (typeof fn !== 'function') {
throw Error('No function provided')
}
return function curriedFn (...args) {
// 判断当前接受的参数是否是小于进行柯里化的函数的参数个数
if(args.length < fn.length) {
// 若是小于的话就返回一个函数再去接收剩下的参数
return function (...argsOther) {
return curriedFn.apply(null, args.concat(argsOther))
}
}else {
return fn.apply(null,args)
}
}
}
const multiply = (x,y,z) => x * y * z;
console.log(curry(multiply)(2)(3)(4))
复制代码
柯里化的应用实例:从数组中找出含有数字的元素
let match = curry(function (expr,str) {
return str.match(expr)
})
let hasNumber = match(/[0-9]+/)
let initFilter = curry(function (fn,array) {
return array.filter(fn)
})
let findNumberInArray = initFilter(hasNumber)
console.log(findNumberInArray(['aaa', 'bb2', '33c', 'ddd', ]))
// 打印 [ 'bb2', '33c' ]
复制代码
咱们上面设计的柯里化函数老是在最后接受一个数组,这使得它能接受的参数列表只能是从最左到最右。
可是有时候,咱们不能按照从左到右的这样严格传入参数,或者只是想部分地应用函数参数。这里咱们就须要用到偏应用这个概念,它容许开发者部分地应用函数参数。
const partial = function (fn, ...partialArgs) {
return function (...fullArguments) {
let args = partialArgs
let arg = 0;
for(let i = 0; i < args.length && arg < fullArguments.length; i++) {
if(args[i] === undefined) {
args[i] = fullArguments[arg++]
}
}
return fn.apply(null,args)
}
}
复制代码
偏应用的示例:
// 打印某个格式化的JSON
let prettyPrintJson = partial(JSON.stringify,undefined,null,2)
console.log(prettyPrintJson({name:'fangxu',gender:'male'}))
// 打印出
{
"name": "fangxu",
"gender": "male"
}
复制代码
const compose = (...fns) => {
return (value) => reduce(fns.reverse(),(acc,fn) => fn(acc), value)
}
复制代码
compose 组合的函数,是按照传入的顺序从右到左调用的。因此传入的 fns 要先 reverse 一下,而后咱们用到了reduce ,reduce 的累加器初始值是 value ,而后会调用 (acc,fn) => fn(acc)
, 依次从 fns 数组中取出 fn ,将累加器的当前值传入 fn ,即把上一个函数的返回值传递到下一个函数的参数中。
组合的实例:
let splitIntoSpace = (str) => str.split(' ')
let count = (array) => array.length
const countWords = composeN(count, splitIntoSpace)
console.log(countWords('make smaller or less in amount'))
// 打印 6
复制代码
compose 函数的数据流是从右往左的,最右侧的先执行。固然,咱们还可让最左侧的函数先执行,最右侧的函数最后执行。这种从左至右处理数据流的过程称为管道(pipeline)或序列(sequence)。
// 跟compose的区别,只是没有调用fns.reverse()
const pipe = (...fns) => (value) => reduce(fns,(acc,fn) => fn(acc),value)
复制代码
定义:函子是一个普通对象(在其它语言中,多是一个类),它实现了map函数,在遍历每一个对象值的时候生成一个新对象。
一、简言之,函子是一个持有值的容器。并且函子是一个普通对象。咱们就能够建立一个容器(也就是对象),让它可以持有任何传给它的值。
const Container = function (value) {
this.value = value
}
let testValue = new Container(1)
// => Container {value:1}
复制代码
咱们给 Container 增长一个静态方法,它能够为咱们在建立新的 Containers 时省略 new 关键字。
Container.of = function (value) {
return new Container(value)
}
// 如今咱们就能够这样来建立
Container.of(1)
// => Container {value:1}
复制代码
二、函子须要实现 map 方法,具体的实现是,map 函数从 Container 中取出值,传入的函数把取出的值做为参数调用,并将结果放回 Container。
为何须要 map 函数,咱们上面实现的 Container 仅仅是持有了传给它的值。可是持有值的行为几乎没有任何应用场景,而 map 函数发挥的做用就是,容许咱们使用当前 Container 持有的值调用任何函数。
Container.prototype.map = function (fn) {
return Container.of(fn(this.value))
}
// 而后咱们实现一个数字的 double 操做
let double = (x) => x + x;
Container.of(3).map(double)
// => Container {value: 6}
复制代码
三、map返回了一传入函数的执行结果为值的 Container 实例,因此咱们能够链式操做。
Container.of(3).map(double).map(double).map(double)
// => Container {value: 24}
复制代码
经过以上的实现,咱们能够发现,函子就是一个实现了map契约的对象。函子是一个寻求契约的概念,该契约很简单,就是实现 map 。根据实现 map 函数的方式不一样,会产生不一样类型的函子,如 MayBe 、 Either
函子能够用来作什么?以前咱们用tap函数来函数式的解决代码报错的调试问题,如何更加函数式的处理代码中的问题,那就须要用到下面咱们说的MayBe函子
让咱们先写一个upperCase函数来假设一种场景
let value = 'string';
function upperCase(value) {
// 为了不报错,咱们得写这么一个判断
if(value != null || value != undefined)
return value.toUpperCase()
}
upperCase(value)
// => STRING
复制代码
如上面所示,咱们代码中常常须要判断一些null
和undefined
的状况。下面咱们来看一下MayBe函子的实现。
// MayBe 跟上面的 Container 很类似
export const MayBe = function (value) {
this.value = value
}
MayBe.of = function (value) {
return new MayBe(value)
}
// 多了一个isNothing
MayBe.prototype.isNoting = function () {
return this.value === null || this.value === undefined;
}
// 函子一定有 map,可是 map 的实现方式可能不一样
MayBe.prototype.map = function(fn) {
return this.isNoting()?MayBe.of(null):MayBe.of(fn(this.value))
}
// MayBe应用
let value = 'string';
MayBe.of(value).map(upperCase)
// => MayBe { value: 'STRING' }
let nullValue = null
MayBe.of(nullValue).map(upperCase)
// 不会报错 MayBe { value: null }
复制代码
MayBe.of("tony")
.map(() => undefined)
.map((x)f => "Mr. " + x)
复制代码
上面的代码结果是 MyaBe {value: null}
,这只是一个简单的例子,咱们能够想一下,若是代码比较复杂,咱们是不知道究竟是哪个分支在检查 undefined 和 null 值时执行失败了。这时候咱们就须要 Either 函子了,它能解决分支拓展问题。
const Nothing = function (value) {
this.value = value;
}
Nothing.of = function (value) {
return new Nothing(value)
}
Nothing.prototype.map = function (fn) {
return this;
}
const Some = function (value) {
this.value = value;
}
Some.of = function (value) {
return new Some(value)
}
Some.prototype.map = function (fn) {
return Some.of(fn(this.value));
}
const Either = {
Some,
Nothing
}
复制代码
函子只是一个实现了 map 契约的接口。Pointed 函子也是一个函子的子集,它具备实现了 of 契约的接口。 咱们在 MayBe 和 Either 中也实现了 of 方法,用来在建立 Container 时不使用 new 关键字。因此 MayBe 和 Either 均可称为 Pointed 函子。
ES6 增长了 Array.of, 这使得数组成为了一个 Pointed 函子。
MayBe 函子极可能会出现嵌套,若是出现嵌套后,咱们想要继续操做真正的value是有困难的。必须深刻到 MayBe 内部进行操做。
let joinExample = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
// 这个时候咱们想让5加上4,须要深刻 MayBe 函子内部
joinExample.map((insideMayBe) => {
return insideMayBe.map((value) => value + 4)
})
// => MayBe { value: MayBe { value: 9 } }
复制代码
咱们这时就能够实现一个 join 方法来解决这个问题。
// 若是经过 isNothing 的检查,就返回自身的 value
MayBe.prototype.join = function () {
return this.isNoting()? MayBe.of(null) : this.value
}
复制代码
let joinExample2 = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
// 这个时候咱们想让5加上4就很简单了。
joinExample2.join().map((value) => value + 4)
// => MayBe { value: 9 }
复制代码
再延伸一下,咱们扩展一个 chain 方法。
MayBe.prototype.chain = function (fn) {
return this.map(fn).join()
}
复制代码
调用 chain 后就能把嵌套的 MayBe 展开了。
let joinExample3 = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
joinExample3.chain((insideMayBe) => {
return insideMayBe.map((value) => value + 4)
})
// => MayBe { value: 9 }
复制代码
Monad 其实就是一个含有 chain 方法的函子。只有of 和 map 的 MayBe 是一个函子,含有 chain 的函子是一个 Monad。
函数式编程主张函数必须接受至少一个参数并返回一个值,可是JavaScript容许咱们建立一个不接受参数而且实际上什么也不返回的函数。因此JavaScript不是一种纯函数语言,更像是一种多范式的语言,不过它很是适合函数式编程范式。
function generateGetNumber() {
let numberKeeper = {}
return function (number) {
return numberKeeper.hasOwnProperty(number) ?
number :
numberKeeper[number] = number + number
}
}
const getNumber = generateGetNumber()
getNumber(1)
getNumber(2)
……
getNumber(9)
getNumber(10)
// 此时numberKeeper为:
{
1: 2
2: 4
3: 6
4: 8
5: 10
6: 12
7: 14
8: 16
9: 18
10: 20
}
复制代码
如今咱们规定,getNumber只接受1-10范围的参数,那么返回值确定是 numberKeeper 中的某一个 value 。据此咱们分析一下 getNumber ,该函数接受一个输入并为给定的范围(此处范围是10)映射输出。输入具备强制的、相应的输出,而且也不存在映射两个输出的输入。
下面我来再看一下数学函数的定义(维基百科)
在数学中,函数是一种输入集合和可容许的输出集合之间的关系,具备以下属性:每一个输入都精确地关联一个输出。函数的输入称为参数,输出称为值。对于一个给定的函数,全部被容许的输入集合称为该函数的定义域,而被容许的输出集合称为值域。
根据咱们对于 getNumber 的分析,对照数学函数的定义,会发现彻底一致。咱们上面的getNumber函数的定义域是1-10,值域是2,4,6,……18,20
文中全部的概念对应的实例能够在 github.com/qiqihaobenb… 获取,能够打开对应的注释来实际执行一下。
《JavaScript ES6 函数式编程入门经典》,强烈建议想入门函数式编程的同窗看一下,书有点老,能够略过工具介绍之类的,关键看其内在的思想,最重要的是,这本书很薄,差很少跟一本漫画书相似。