JavaScript函数式编程入门典范

一个延续更新的github笔记,链接地点:Front-End-Basics,可以watch,也可以star。

此篇文章的地点:JavaScript函数式编程入门典范

正文最先

什么是函数式编程?为什么它重要?

数学中的函数

f(x) = y
// 一个函数f,以x为参数,并返回输出y

症结点:

  • 函数必需老是吸收一个参数
  • 函数必需老是返回一个值
  • 函数应当依据吸收到的参数(比方x)而不是外部环境运转
  • 关于一个给定的x,只会输出唯一的一个y

函数式编程手艺重要基于数学函数和它的头脑,所以要明白函数式编程,先相识数学函数是有必要的。

函数式编程的定义

函数是一段可以经由历程其称号被挪用的代码。它可以吸收参数,并返回值。

与面向对象编程(Object-oriented programming)和历程式编程(Procedural programming)一样,函数式编程(Functional programming)也是一种编程范式。我们可以以此竖立仅依靠输入就可以完成本身逻辑的函数。这保证了当函数被屡次挪用时依然返回雷同的效果(援用通明性)。函数不会转变任何外部环境的变量,这将发生可缓存的,可测试的代码库。

函数式编程具有以下特性

1、援用通明性

一切的函数关于雷同的输入都将返回雷同的值,函数的这一属性被称为援用通明性(Referential Transparency)

// 援用通明的例子,函数identity不管输入什么,都邑一成不变的返回
var identity = (i) => {return i}
替代模子

把一个援用通明的函数用于其他函数挪用之间。

sum(4,5) + identity(1)

依据援用通明的定义,我们可以把上面的语句换成:

sum(4,5) + 1

该历程被称为替代模子(Substitution Model),因为函数的逻辑不依靠其他全局变量,你可以直接替代函数的效果,这与它的值是一样的。所以,这使得并发代码缓存成为能够。

并发代码: 并发运转的时刻,假如依靠了全局数据,要保证数据一致,必需同步,而且必要时须要锁机制。遵照援用通明的函数只依靠参数的输入,所以可以自在的运转。

缓存: 因为函数会为给定的输入返回雷同的值,现实上我们就可以缓存它了。比方完成一个盘算给定数值的阶乘的函数,我们就可以把每次阶乘的效果缓存下来,下一次直接用,就不用盘算了。比方第一次输入5,效果是120,第二次输入5,我们晓得效果一定是120,所以就可以返回已缓存的值,而没必要再盘算一次。

2、声明式和笼统

函数式编程主意声明式编程和编写笼统的代码。

比较敕令式和声明式
// 有一个数组,要遍历它并把它打印到控制台

/*敕令式*/
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

// 我们运用了一个处置惩罚“怎样”做的笼统函数,然后我们就可以只体贴做“什么”了
函数式编程主意以笼统的体式格局竖立函数,比方上文的forEach,这些函数可以在代码的其他部分被重用。

3、纯函数

大多数函数式编程的优点来自于编写纯函数,纯函数是对给定的输入返回雷同的输出的函数,而且纯函数不该依靠任何外部变量,也不该转变任何外部变量。

纯函数的优点

  1. 纯函数发生轻易测试的代码
  2. 纯函数轻易写出合理的代码
  3. 纯函数轻易写出并发代码

纯函数老是许可我们并发的实行代码。因为纯函数不会转变它的环境,这意味着我们基本不须要忧郁同步题目。

  1. 纯函数的输出效果可缓存

既然纯函数老是为给定的输入返回雷同的输出,那末我们就可以缓存函数的输出。

高阶函数

数据和数据范例

递次作用于数据,数据关于递次的实行很重要。每种编程言语都有数据范例。这些数据范例可以存储数据并许可递次作用个中。

JavaScript中函数是一等国民(First Class Citizens)

当一门言语许可函数作为任何其他数据范例运用时,函数被称为一等国民。也就是说函数可被赋值给变量,作为参数通报,也可被其他函数返回。

函数作为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"
}

组合与管道

Unix的理念

  1. 每一个递次只做好一件事变,为了完成一项新的使命,从新构建要好过在庞杂的旧递次中增添新“属性”。
  2. 每一个递次的输出应当是另一个尚未可知的递次的输入。
  3. 每一个基本函数都须要吸收一个参数并返回数据。

组合(compose)

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)

函子

什么是函子(Functor)?

定义:函子是一个一般对象(在别的言语中,多是一个类),它完成了map函数,在遍历每一个对象值的时刻天生一个新对象。

完成一个函子

1、简言之,函子是一个持有值的容器。而且函子是一个一般对象。我们就可以竖立一个容器(也就是对象),让它可以持有任何传给它的值。

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}

2、函子须要完成 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}

3、map返回了一传入函数的实行效果为值的 Container 实例,所以我们可以链式操纵。

Container.of(3).map(double).map(double).map(double)
// => Container {value: 24}

经由历程以上的完成,我们可以发明,函子就是一个完成了map左券的对象。函子是一个追求左券的观点,该左券很简朴,就是完成 map 。依据完成 map 函数的体式格局差别,会发生差别范例的函子,如 MayBe 、 Either

函子可以用来做什么?之前我们用tap函数来函数式的处理代码报错的调试题目,怎样越发函数式的处置惩罚代码中的题目,那就须要用到下面我们说的MayBe函子

MayBe 函子

让我们先写一个upperCase函数来假定一种场景

let value = 'string';
function upperCase(value) {
  // 为了防止报错,我们得写这么一个推断
  if(value != null || value != undefined)
    return value.toUpperCase()
}
upperCase(value)
// => STRING

如上面所示,我们代码中常常须要推断一些nullundefined的状况。下面我们来看一下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 }

Either 函子

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
}

Pointed 函子

函子只是一个完成了 map 左券的接口。Pointed 函子也是一个函子的子集,它具有完成了 of 左券的接口。 我们在 MayBe 和 Either 中也完成了 of 要领,用来在竖立 Container 时不运用 new 症结字。所以 MayBe 和 Either 都可称为 Pointed 函子。

ES6 增添了 Array.of, 这使得数构成为了一个 Pointed 函子。

Monad 函子

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许可我们竖立一个不吸收参数而且现实上什么也不返回的函数。所以JavaScript不是一种纯函数言语,更像是一种多范式的言语,不过它异常合适函数式编程范式。

补充

1、纯函数是数学函数

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

2、实例

文中一切的观点对应的实例可以在 https://github.com/qiqihaobenben/learning-functional 猎取,可以翻开对应的解释来现实实行一下。

3、荐书

《JavaScript ES6 函数式编程入门典范》,强烈建议想入门函数式编程的同砚看一下,书有点老,可以略过东西引见之类的,症结看其内涵的头脑,最重要的是,这本书很薄,差不多跟一本漫画书类似。

4、引荐文章(非援用文章)

  1. 座谈 JS 函数式编程(一)
  2. 从一道坑人的面试题说函数式编程
  3. 函数式编程入门教程
  4. 函数式编程的一点实战
    原文作者:方旭
    原文地址: https://segmentfault.com/a/1190000019069277
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞