导语
本篇文章为阅读笔记 ,文章内容为 《JavaScriopt ES6 函数式编程入门经典》
函数式编程简介
什么是函数式编程,为何他重要
数学中的函数
f(x) = y
// 一个函数f,以x为参数,并返回输出y
关键点:
- 函数必须总是接受一个参数
- 函数必须总是返回一个值
- 函数应该依据接受到的参数,而不是外部环境运行
- 对于一个给定的x,只会输出唯一的一个y
引用透明性
所有的函数对于相同的输入都将返回相同的值,函数的这一属性被称为 引用透明性(Referential Transparency)
// 引用透明的例子,函数identity无论输入什么,都会原封不动的返回
var identity = (i) => {return i}
替换模型
把一个引用透明的函数用于其他函数调用之间。
sum(4,5) + identity(1)
根据引用透明的定义,我们可以把上面的语句换成:
sum(4,5) + 1
该过程被称为替换模型(Subsitution 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
// 我们使用了一个处理“如何”做的抽象函数,然后我们就能只关心做“什么”了
函数式编程主张以抽象的方式创建函数,如上文的forEach,这些函数能够在代码的其他部分被重用.
纯函数
纯函数是对给定的输入返回相同的输出的函数,并且纯函数不应依赖任何外部变量,也不应改变任何外部变量。
- 高并发: 纯函数总是允许我们并发地执行代码,因为纯函数不会改变他的环境
- 可缓存: 既然纯函数总是为给定的输入返回相同的输出,那么我们就能够缓存函数的输出。
高阶函数
接受另一个函数作为其参数的函数或返回一个函数的函数称为高阶函数(Higher-OrderFunction)
javaScript 数据类型
- Undefined
- BigInt
- String
- Null
- Number
- Boolean
- Object
函数作为JavaScript的一种数据类型,由于函数是类似String的数据类型所以我们能把函数存入一个变量,能够作为函数的参数进行传递.所以JavaScript中函数是一等公民.当一门语言允许函数作为任何其他数据类型使用时,函数被称为一等公民.也就是说函数可被赋值给变量,作为参数传,也可被其他函数返回.
抽象与高阶函数
一般而言,高阶函数通常用于抽闲通用的问题,换句话说,高阶函数就是定义抽象
抽象: 在软件工程和计算机科学中,抽象是一种管理计算机系统复杂性的技术。 通过建立一个人与系统进行交互的复杂程度,把更复杂的细节抑制在当前水平之下,简言之,抽象让我们专注于预定的目标而无须关心底层的系统概念.
// 用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))
闭包与高阶函数
闭包就是一个内部函数.什么是内部函数? 就是在另一个函数内部的函数.
闭包的强大之处在于它对作用域链(或作用域层级)的访问
闭包的三个可访问的作用域
- 在它自身声明之内声明的变量
- 对全局变量的访问
- 对外部函数变量的访问(重点)
//全局
let globalStr = 'global';
function outer(){
//外部变量
let outerStr = 'outer'
function inter(){
//自身变量
let interStr = 'inter'
console.log(globalStr)
console.log(outerStr)
console.log(interStr)
}
inter();
}
outer();
记住闭包的位置
闭包中重要的概念--闭包可以记住它的上下文!!
//全局
let globalStr = 'global';
function outer(){
//外部变量
let outerStr = 'outer'
function inter(){
//自身变量
let interStr = 'inter'
console.log(globalStr)
console.log(outerStr)
console.log(interStr)
}
return inter
}
let inter = outer();
inter()
实现一个tab函数
tab函数接受一个value 并返回valued的闭包函数,函数将被执行
let tap = (value)=>
(fn)=> (
typeof(fn) === 'function' && fn(value),
console.log(value)
)
tap('func')((it)=>console.log('value of '+it))
//输出
value of func
func
实现一个reduce函数
const reduce = (array,fn,initialValue)=>{
let accumlator;
if(initialValue != undefined)
accumlator = initialValue;
else
accumlator = array[0];
if(initialValue == undefined)
for(let i = 1;i<array.length;i++)
accumlator = fn(accumlator,array[i])
else
for(const value of array)
accumlator = fn(accumlator,value)
return [accumlator]
}
let total = reduce([1,2,3,4],(acc,val)=>acc*val,1)
console.log(total) //[24]
柯里化与偏应用
专业术语
一元函数
只接受一个参数的函数称为一元(unary)函数
二元函数
接受两个参数的函数称为二元(binary)函数
变参函数
变参函数是接受可变数量的函数
const variadic = (a,...variadic){
console.log(a)
console.log(variadic)
}
柯里化
柯里化是把一个多参数函数转换成为一个嵌套一元函数的过程
//普通版本
const add = (x,y)=>x+y
add(1,4)
// 柯里化版本
const addCurried = x => y => x + y
addCurried(1)(3)
//写一个高阶函数.把add 转换成addCurried 的形式
const curry = (binarFn)=>{
return function(firstArg){
return function(secondArg){
binarFn(firstArg,firstArg)
}
}
}
let autoCurriedAdd = curry(add)
autoCurriedAdd(2)(3)
实现多参数版的函数柯里化
const curry = (fn)=>{
if(typeof fn !== 'function'){
throw Error('No function pvovided')
}
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', ]))
数据流
偏应用
上面设计的柯里化函数总是在最后接受一个数组,这使得它能接受的参数列表只能是从最左到最右
但是有时候,我们不能按照从左到右的这样严格传入参数,或者只是想部分地应用函数参数。这里我们就需要用到偏应用这个概念,它允许开发者部分地应用函数参数。
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的理念
Unix的理念是由Ken Thompson 提出的一套思想.
- 每个程序只做好一件事情.为了完成这一项任务,重新构建要好在于复杂的旧程序中添加新"属性"
- 每个程序的输出应该是另一个尚未可知的程序的输入
组合(compose)函数
compose 组合的函数,是按照传入的顺序从右到左调用的。所以传入的 fns 要先 reverse 一下,然后我们用到了reduce ,reduce 的累加器初始值是 value ,然后会调用 (acc,fn) => fn(acc), 依次从 fns 数组中取出 fn ,将累加器的当前值传入 fn ,即把上一个函数的返回值传递到下一个函数的参数中。
基础版
const compose = (a,b)=>
(c)=>a(b(c))
//使用
let number =compose(Math.round,parseFloat)
console.log(number('3.34')) //3
多函数版
const compose = (...fns)=>
(value)=>
reduce(fns.reverse(),(acc,fn)=>fn(acc),value )
管道/序列
compose函数的数据流是从右往左的,最右侧的先执行.我们也可以让最左侧的函数先执行,最右侧的后执行.这种从左至右的处理数据流的过程称为管道(pipeline)或序列(sequence).
// 跟compose的区别,只是没有调用fns.reverse()
const pipe = (...fns) => (value) => reduce(fns,(acc,fn) => fn(acc),value)
函子
它将用一种纯函数式的方式帮助我们处理错误
什么是函子(Functor)
定义: 函子是一个普通对象(在其他语言中,可能是一个类),它实现了map函数,在遍历每个对象值得时候生成一个新的对象.
实现函子
1、简言之,函子是一个持有值的容器。而且函子是一个普通对象。我们就可以创建一个容器(也就是对象),让它能够持有任何传给它的值。
const Container = function(val){
this.value = val
}
let testValue = new Container(2)
//=> Container {value:2
给Container 增加一个静态方法,它可以为我们在创建新的Container 时省略new 关键字
Container.of = function (value) {
return new Container(value)
}
// 现在我们就可以这样来创建
Container.of(1)
// => Container {value:1}
2.函数需要实现map方法,具体的实现是,map函数从Container中取出值,传入的函数把取出的值作为参数调用,并将结果放回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 **
MayBe 函子
它将将以更加函数式的处理代码中的错误问题
会判断值为 undefined 和 null 的情况
// MayBe 跟上面的 Container 很相似
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 是一个函子
参考
- 《javaScript函数式编程入门经典》
- 文章作者 JavaScript函数式编程入门经典
结语
前端界的一枚小学生!!!