函数式编程、纯函数、高阶函数、闭包、柯里化函数,偏应用,组合和管道,函子,Generator

>>函数式编程:

原则:小

f(x) = y[数学函数]
1、函数必须总是接受一个参数;
2、函数必须总时返回一个值;
3、函数应该依据接收到的参数(例如x),而不是外部环境运行;
4、对于一个给定的x,只会输出唯一一个y。

函数式编程是一种范式,我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数被多次调用时任然
返回相同的结果。函数不会改变任何外部环境的变量,这将产生可缓存的,可测试的代码库。

  • 引用透明性:所有的函数对于相同的输入都将返回相同值

  • 替换模型:直接替换函数的结果(主要因为函数的逻辑不依赖其他全局的变量),这使并发代码和缓存成为可能。

函数式编程主张声明式编程编写抽象的代码
(区分声明式编程和命令式编程:for 和 forEach)

“如何”做的部分将被抽象到普通函数中(高阶函数)

函数式编程主张以抽象的方式创建函数,这些函数能够在代码的其他部分被重用。

抽象:把复杂的东西抽出来,变成简单的东西。


>>纯函数:

纯函数是数学函数

纯函数是对给定的输入返回相同的输出的函数
->产生可测试的代码(纯函数不应改变任何外部环境的变量)
->合理的代码:必须具有一个有意义的名称(通过函数名能够轻易地推理出该函数的作用)
=>包含纯函数的代码易于阅读、理解和测试。

js不是一种纯函数语言(因为可以不用参数传入)

js支持函数作为参数,以及将函数传值给另一函数等特性(js将函数视为一等公民)


>>高阶函数:

高阶函数:接受另一个函数作为其参数的函数称为高阶函数。

// JS七种基本类型
{
  Number
  String
  Boolean
  Object
  Null
  Undefined
  Symbol
}

*在《JavaScript权威指南》中把function被看做是object基本数据类型的一种特殊对象,另外《悟透JavaScript》和《JavaScript高级程序设计》也把函数视为对象,而不是一种基本数据类型。

高阶函数是接受函数作为参数并且/或者返回函数作为输出的函数。

大多数高阶函数与闭包一起使用。


>>闭包

闭包与作用域相关联:

outer(){
     inner(){
    ...
  }
}

inner是闭包,inner能访问自身作用域的变量,也能访问全局变量,也能访问外部函数父级(outer)作用域的变量。

  • 闭包能访问外部函数的变量,该属性使闭包变量非常强大!
  • 闭包可以记住它的上下文。

>>柯里化函数(curry):

(高阶函数和闭包构成柯里化函数)

把一个多参数函数转换为一个嵌套的一元函数的过程;
注:curry函数应用参数列表的顺序是从左到最右

  • 两元函数转柯里化
let curry = (fn) => {
            return function (x) {
                return function (y) {
                    return fn(x,y)
                }
            }
        }

let add = function (x,y) {
            return x+y
        }

curry(add)(2)(3) // => 5
  • 多参数函数转化一元函数的curry函数
let curryN = (fn) => {
            if( typeof fn !== 'function' ){
                throw Error('no function provided');
            }
            return function curriedFn (...args) {
                if(args.length < fn.length){
                    return function () {
                        return curriedFn.apply(null,args.concat([].slice.call(arguments)));
                    }
                }
                return fn.apply(null,args);
            }
        }

let add = function (x,y,z,o,p) {
            return x+y+z+o+p
        }

curry(add)(2)(3)(4)(5)(6) // => 20

应用场景:

// 封装调试console
const loggerHelper = (mode,initialMsg,errorMsg,lineNo) => {
  if(mode === 'DeBug'){
    console.debug(initialMsg,errorMsg+"at line:"+lineNo)
  }else if(mode === 'Log'){
    console.log(initialMsg,errorMsg+"at line:"+lineNo)
  }else {
    throw Error("Wrong mode")
  }
}
//  利用柯里化转化
let debugLogger = curryN(loggerHelper)('Log')("DeBug at stats.js")
debugLogger('debug message',123) // => DeBug at stats.js debug messageat line:123

// 在数组内容中查找数字
let match = curryN(function (expr,str) {
  return str.match(expr)

})

let hasNumber = match(/[0-9]+/)
        
let filter = curryN(function (fn,arr) {
  return arr.filter(fn)
})

let findNumbersInArray = filter(hasNumber)

findNumbersInArray(['is','number2']) // =>['number2']
// 求数组的平方
let map = function (x) {
  return x*x
}

let squareAll = curryN(function (fn,arr) {
  return arr.map(fn)
})

console.log(squareAll(map)([2,3,4]))

>>偏函数(partial):

产生背景:curry函数应用参数列表的顺序是从左到最右,但有时候我们需要颠倒某两个参数的顺序

const partial = function (fn,...partialArgs) {
            let args = partialArgs;
            return function (...fullArguments) {
                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);
            }
        }

应用场景:

// 定时执行
let delayTenMsg = partial(setTimeout,undefined,10)
delayTenMsg(()=>{
  console.log('执行结束')
})

partial应用于任何含有多个参数的函数:

let obj = {
  foo: 'bar',
  bar: 'foo'
}
JSON.stringify(obj,null,2)

// 偏函数改造:
let prettyPrintJSON = partial(JSON.stringify,undefined,null,2)
prettyPrintJSON(obj) 

注:这偏函数实现中有一个小bug,只能是一次性的,因为我们是用undefined替换partialArgs,而数组传递的是引用!因而当你第二次传参let obj2 = { foo: 'bar2', bar: 'foo2' }打印出来的值还是上一次的obj结果。

总结:什么时候用curry,什么时候用partial,归结于使用场景,curry的传参必须从左到右依次执行,partial用于解决这种不能从左到右依次执行的情况。

柯里化和偏应用是函数式编程的两种重要技术,作为一名js程序员,应该在代码库中选择柯里化或偏应用直一。


>>组合和管道:

函数式组合在函数式中被称为组合。

Unix的理念:

  • 每个程序只做好一件事。为了完成一项新的任务,重新构建要好于在复杂的旧程序中添加新属性;

  • 每个程序的输出应该是另一个尚未知的程序输入

  • 管道( | )在两个命令之间扮演了桥梁的角色

  • 基础函数需要遵循如下规则:每一个基础函数都需要接收一个参数并返回数据!

组合函数的真正优势在于:无须创建新的函数就可以通过基础函数解决眼前的问题。

compose函数定义:

  • 两元组合函数:
const compose = (a,b)=> c =>a(b(c))

应用:

let number = compose(Math.round,parseFloat)
number('6.63') // =>7
let splitIntoSpace = (str) => str.split(" ")
let count = arr => arr.length
let countWords = compose(count,splitIntoSpace)
countWords("hello i am your dad") // =>5
  • 多元组合函数:
const composeN = (...fns) => value => {
  let _fns = fns.reverse()
  return _fns.reduce((acc,fn)=>fn(acc),value)
}

应用:

let splitIntoSpace = (str) => str.split(" ")
let count = arr => arr.length
let addOrEven = num => num%2 == 1 ? 'even' : 'add' // 判断奇数还是偶数
let countWords = composeN(addOrEven,count,splitIntoSpace)
composeN(addOrEven,count,splitIntoSpace) // => "even"

pipe函数定义:

与compose函数所作的事请相同,只不过交换了数据流的方向;
pipe和compose在现实开发中应该二选一,否则会让开发者感到困扰。

const pipe = (...fns) => value => {
  return fns.reduce((acc,fn)=>fn(acc),value)
}

组合满足结合律:
composeN(f, composeN(g,h)) = composeN(composeN(f, g),h)

identity函数定义:

用于定位组合函数的数据流中哪个出错

const identity = (it) =>{
            console.log(it)
            return it
        }
let countWords2 = composeN(addOrEven,count,identity,splitIntoSpace) 
// => ["hello", "i", "am", "your", "dad"]
// => "even"

>>函子:

(错误处理)
函子,它将用一种纯函数式的方式帮助我们处理错误;
函子是一个普通对象(在其他语言中,可能是一个类),它实现了map函数,在遍历每对象值得时候生成一个新对象。

const Container = function (value) {
  this.value = value
}

Container.of = function(val){
  return new Container(val)
}

// 函子实现 map方法:
Container.prototype.map = function (fn) {
  return Container.of(fn(this.value))
}


Container.of({a:3}) // =>Container {value: {a:3}}
Container.of([1,2,3]) // =>Container {value: [1,2,3]}
Container.of(3).map(e=>e+1) // =>Container {value: 4}

支持链式操作:

Container.of(3).map(e=>e+1).map(e=>e*e) // =>16

换句话讲:函子是一个实现了map契约的对象!

MayBe函子:
利用函数式编程技术处理错误或异常的问题;
任何层级的链式map都会被调用(管你传了null还undefined)该过程将连接到链条中的最后一个map函数被调用。

const MayBe = function (value) {
  this.value = value
}

MayBe.of = function(val){
  return new MayBe(val)
}

MayBe.prototype.isNothing = function () {
  return (this.value === null || this.value === undefined)
}

// 函子实现map方法:
MayBe.prototype.map = function (fn) {
  return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value))
}

// 应用
MayBe.of('Roy is a man').map(e=>e.toUpperCase()).map(e=> "Mr. " + e) // MayBe {value: "Mr. ROY IS A MAN"}

拓展:ES5的类为什么不可以用箭头函数,因为其不具有[[constructor]]和prototype属性

Either函子:
Either的出现是为了解决MayBe不能返回undefined或null值执行失败的状态

const Nothing = function (value) {
  this.value = value
}

Nothing.of = function(val){
  return new Nothing(val)
}

Nothing.prototype.map = function (fn) {
  return this // 返回对象本身
}

const Some = function (value) {
  this.value = value
}

Some.of = function(val){
  return new Some(val)
}

Some.prototype.map = function (fn) {
  return Some.of(fn(this.value))
}

// Either定义
const Either = {
  Some: Some,
  Nothing: Nothing
}

(案例代码见代码清单8-14)

Pointed函子:
函子只是一个实现了map契约的接口
Pointed函子是一个函子的子集,它具有实现了of契约的接口

ES6增加了Array.of,这使数组成为一个Pointed函子!

Array.of("you are a pointed functor,too?") // =>["you are a pointed functor,too?"]

Monad函子:
含有chain的Pointed函子被称为Monad函子。

  • join解决问题

通过join解决问题,解决map深层次嵌套问题

为MayBe函子添加一个join方法:将嵌套的结构展开为一个单一层级

MayBe.prototype.join = function () {
  return this.isNothing() ? MayBe.of(null) : this.value
}
// join方法虽很简单,但却能帮助我们打开嵌套的MayBe
let joinExample = MayBe.of(MayBe.of(5)) // => MayBe { value : MayBe { value : 5} }
joinExample.join() //  => MayBe { value : 5}
  • 实现chain
    我们总是要在map后调用join,下面把逻辑封装在一个名为chain的方法中。
MayBe.prototype.chain = function (fn) {
  return this.map(fn).join
}

总结:重复的map调用会导致嵌套问题,chain能帮助扁平化MayBe数据


>>Generator

作用:用generator的next调用替换回调

// 这里的业务场景:先通过接口1获取picturesJson数据,然后通过picturesJson数据的url字段,再次请求接口2,最终获得第一张图的数据
function request(url) {
  httpGetAsync(url,function (reponse) {
    generator.next(reponse)
  })
}

function *main() {
  let picturesJson = yield request('https://www.xxx.com/pics/.json')
  let firstPictureData = yield  request(picturesJson.data.children[0].data.url + '.json')
  console.log(firstPictureData) // 最后成功打印结果
}

// 执行
let generator = main()
generator.next()

通过Generator解决了地狱回调问题。

思考总结:Generator的应用场景主要用于解决某个数据需要等到上一个异步请求结果,早期这种情况,我们会选择回调的方法解决,但是如果遇到多个回调嵌套的问题,就会造成代码不好维护(地狱回调),选择Generator替代回调,代码会变得更加直观更好维护。

推荐阅读更多精彩内容