函数式编程

309 阅读10分钟

我们为什么现在要函数式编程

  • 函数式编程是随着React的流行受到越来越多的关注
  • Vue3也开始拥抱函数式编程
  • 函数式编程也可以抛弃this
  • 打包过程中可以更好的利用three shaking过滤无用代码
  • 方便测试,方便并行处理
  • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda

什么是函数式编程

函数式编程(function programming,FP),FP是编程范式之一,我们常听说的编程范还有面向过程编程、面向对象编程。

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系;
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

函数式编程思维方式的理解

  • 程序的本质:根据输入某种运算获得相应的输出,程序开发古城中涉及很多有输入和输出的函数

  • x->f(联系、映射)->y,y=f(x),输入x,通过某种联系得到结果y,这里面的联系f就是我们的运算过程(也就是函数式编程的函数),通过f(x)得到y;

  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sin(x),x和y的关系

  • “相同的输入始终要得到相同的输出”(纯函数)

  •  总结:函数式编程用来描述数据(函数)之间的映射,对其数据运算过程的抽象
    

函数式编程和面向对象编程的不同

  •  从思维方式上来说面向对象编程是对事物的抽象,二函数式编程式对运算过程的抽象
    

函数是一等公民:MDN First-class Function

一等公民可以作为:函数可以存储在变量中;函数作为参数;函数作为返回值; 例如:字符串在几乎所有编程语言中都是一等公民,字符串可以作为函数参数 、函数返回值、赋值给变量。

在JavaScript中函数就是一个"普通的对象"(可以通过new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function(alert(‘1’)),因此JavaScript中函数是一等公民。

来构造一个新函数;

  • 把函数赋值给变量
let fn = function() {
    console.log('Hello First-class Function')
}
fn()
//一个示例
const Blogcontroller = {
    index(posts) {return Views.index(posts)} //index方法和内部调用的方法有相同的形式,参数和返回值相同;一个函数包含一个函数,形式相同,其实就是相同的函数
    show (post) { return Views.show(post) },
    create (attrs) { return Db.create(attrs) },
    update (post, attrs) { return Db.update(post, attrs) },
    destroy (post) { return Db.destroy(post) }
}
//优化:把函数的一个方法赋值另一个方法(赋值方法本身),而不是把函数的调用赋值给另一个方法
const Blogcontroller = {
    index: Views.index //复制方法本身
    show: Views.show
    create: Db.create
    update: Db.update
    destroy: Db.destroy
}

高阶函数

概念:高阶函数 可以把“函数作为参数” 传递给另一个函数;可以把函数作为另一个函数的“返回结果”

高阶函数是用来抽象通用的问题高阶函数----作为参数;函数作为参数的话,参数会变得更加灵活,调用函数的方法forEach不用考虑函数是如何实现的;

一、 函数作为参数传递

模拟forEach:遍历数组每个数字,并且返回数组的每一项

例如:函数参数:2个参数,第一个参数首先传一个数组,第二个是参数,遍历数组后, 每次对数组的处理操作是不同的,内容是变化的

function forEach( array, fn) {
    for( let i =0; i< array.length; i++) {
        fn(array[i])
    }
}

// 测试
let  arr = [1,2,3,4,7,8]
//item,用来接受item的每一项
forEach(arr,function(item){
    console.log(item)
})

//模拟:filter:过滤数组中满足条件的数据存储,返回最终结果
function fliter(arr, fn) {
    let   results = [] //存储结果
    for(let i=0; i< arr.length; i++){
        if(fn(arr[i])){ // 数组是否满足条件的每一项
            results.push(arr[i])
        }
    }
    return results
}

// 测试
let arrFliter= [2,1,56,8]
let f = fliter(arrFliter, function (item){
    return item % 2 == 0
})
console.log('过滤的结果',f)

二、函数作为作为返回值

高阶函数-----作为返回值,就是函数生成一个函数

/*
*高阶函数语法展示
*/
function makeFn() {
    let msg = 'Hello msg'
    return function () {
        console.log(msg)
    }
}
//调用方式一
const fn = makeFn()
fn()
// 调用方式二
makeFn()()

//once: 只让函数只执行一次,场景:在支付的时候,一个订单,用户点击多次按钮,只让用户执行一次
function once (fn) {
    let done = false //fn没有执行
    return function () {
        if(!done) {
            done = true
            fn.apply(this,arguments)
        }
    }
}
//测试
let pay = once(function (money) {
    console.log(`支付: ${money} RMB`)
})
pay(5)
pay(5)
pay(5)

使用高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
  • 高阶函数是用来 “ 抽象通用 “的问题 高阶函数: forEach filter 例子如上

常用的高阶函数

forEach、map、 filter、 every、 some、 find/findIndex、 reduce、 sort

// 模拟常用高阶函数:map every some

// map:对数组的元素进行遍历,并对每一项进行处理,处理结果并返回新数组
const map = (array, fn) => {
    let results = [] // 新数组
    for (let value of array) {
        results.push(fn(value))
    }
    return results
}
//测试map
let arr = [1,2,34,5,4]
arrMap = map(arr, v => v*v)
console.log('map',arrMap)

// every:判断数组的每一项是否符合条件,如果有一项不符合,就返回false;返回true/false
 const every = (array, fn) => {
     let result = true
     for(let value of array) {
         result = fn(value)
         if(!result) {
            break
         }
     }
     return result
 }

 // 测试every
 let  arrE = [122,33,11,14]
 let r = every(arrE, v=> v>10)
 console.log('every', r)

 //some:检测数组中的元素是否有一个满足数组中的条件,有一个满足返回
 const some = (array, fn) => {
    let result = false
    for(let value of array) {
        result = fn(value)
        if(result) {
            break
        }
    }
    return result
 }

 // 测试 
 let arrSome = [1,3,5,9]
 let sr = some(arrSome, v=>v % 2 === 0)
 console.log('some', sr)

闭包

概念:闭包(Closure)函数和其他周围的状态(词法环境)的引用捆绑在一起形成闭包。 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员,

在另一个作用域中可以调用manFn()的内部函数 当我们调用这个内部函数的时候,可以访问到manFn()的内部成员

function makeFn() {
    let msg = 'Hello msg'
    return function () {
        console.log(msg)
    }
}
函数里面返回了一个函数,并且返回的函数内部引用了外部函数的成员,就形成了闭包
//调用方式一
makeFn就是引用了内部的函数,被外部引用就不能释放
const fn = makeFn()
fn()

闭包的本质

函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员 因为被外部引用不能释放 ,因此内部函数依然可以访问外部函数的成员

闭包案例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0, user-scalable=no">
  </head>
  <body>
      <script>
        //求员工的工资
        // getSalary(12000, 2000) 
        // getSalary(15000, 3000)
        // getSalary(15000, 4000) 
//为不同级别的员工计算工资,传入基本工资加绩效
        function makeSalary(base) {
          return function (performance) {
            return base + performance
          }
        }
        let salaryLever1 = makeSalary(12000)
        let salaryLever2 = makeSalary(15000)

        console.log(salaryLever1(2000))
        console.log(salaryLever2(2000))
      </script>
  </body>
</html>

纯函数

纯函数的概念

  • 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
  • 函数式编程 不会保留计算中间 的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
  • 数组的 slice 和 splice 分别是:纯函数和不纯的函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作返回该数组,会改变原数组
// slice /splice

// 纯函数 相同输入得到相同的输出
let array = [1,2,34,5]
console.log(array.slice(0,3)) //没有改变原数组
console.log(array.slice(0,3)) 
console.log(array.slice(0,3)) 

console.log(array.splice(0,3)) // 改变原数组,返回新数组
console.log(array.splice(0,3))
console.log(array.splice(0,3))

lodash

// 初始化,package.json  npm init -y  ===>  npm i lodash
// first lasr toUpper reverse each includes find findeIndex
const _ = require('lodash')

const array = ['jack', 'tom', 'lucy', 'kate']

console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.first(array)))
console.log(_.reverse(array))

const r = _.each(array, (item, index) =>{
    console.log(item, index)
})
console.log(r)

纯函数的好处

  • 可缓存:因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的课过缓存起来
// 记忆函数
const _ = require('lodash')

function getArea() {
    return Math.PI*r*r
}

let getAreaWithMemory = _.memoize(getArea) 
console.log(getAreaWithMemory(4))
//从缓存中取数据
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))

//模拟memoize的实现
function memoize (f) {
    //把f存储
    let cache = {}
    return function () {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || f.apply(f, arguments) 
        return cache[arg_str]
    }
}
  • 可测试:纯函数让测试更加方便

  • 并行处理

    • 在多线处理环境下并行操作共享的内存数据很可能出现意外情况
    • 纯函数不需要共享的内存数据,所以在并行环境下可以任意运行纯函数

纯函数的副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯的

let mini = 18

function checkAge (age) {

return age >= mini

}

// 纯的(有硬编码,后续可以通过柯里化解决)

function checkAge (age) {

let mini = 18

return age >= mini

}

副作用让一个函数变的不纯(如上例:全局变量),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

副作用来源:

  • 配置文件
  • 数据库
  • 获取用户的输入

所有的 “外部交互 ”都有可能带来 ”副作用“, 副作用也使得方法通用性下降不适合扩展和可重用性, 同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化

使用柯里化解决上一个案例只能够硬编码问题

// 柯里化演示
function checkAge(age) {
    let min = 18
    return age >= min
}

// // 普通的纯函数
function checkAge (min, age) {
    return age >= min
}
console.log(checkAge(18,20))
console.log(checkAge(18,24))
console.log(checkAge(22,24))

//柯里化
function checkAge(min) {
    return function(age) {
        return age >= min
    }
}

let check18 = checkAge(18)
let check20 = checkAge(20)

console.log(check18(20))
console.log(check20(22))

//es6优化
let checkAge = min => (age => age >= min) 
let check22 = checkAge(22)
console.log(check22(20))

柯里化

  • 当一个函数有多个参数的时候先传递题部分参数调用它(这部分参数以后永远不变) 然后返回一个新的函数接受剩余的参数,返回结果

lodash中的柯里化函数

  • _curry(func)
    • 功能:创建一个函数,该函数接受一个或者多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接受剩余的参数
    • 参数:需要柯里化的函数
    • 返回值:柯里化后的函数
const _= require('lodash')

//要柯里化的函数
function getSum(a, b, c) {
    return a + b + c
}

const curried = _.curry(getSum)

//接受全部参数
console.log(curried(1,2,3))

// 接受部分参数,返回新的一个函数,等待接收剩余的参数
console.log(caches(1)(2,3))

console.log(curried(1,2)(3))

案例

//柯里化案例:lodash curry
//判断一个字符串是否有空字符,或者提取字符串所有空字符, match
const _= require('lodash')

// 柯里化
 const match = _.curry(function(reg, str) {
    return str.match(reg)
})

const haveSpace = match(/\s+/g) 
const haveNumber = match(/\d+/g)

console.log(haveSpace('hello world'))
console.log(haveNumber('1234abcd'))

// 过滤数组空白字符, filter

const filter = _.curry(function(func, array) {
    return array.filter(func)
})
console.log(filter(haveSpace, ['John Conner', 'John_Donner']))

// 过滤数组空白字符, filter 优化 
const findSpace = filter(haveSpace)
console.log(findSpace(['John Conner', 'John_Donner']))

模拟实现lodash中curry的方法

//模拟实现lodash中curry的方法
const _= require('lodash')

//要柯里化的函数
function getSum(a, b, c) {
    return a + b + c
}

// const curried = _.curry(getSum) // lodash参数
const curried = curry(getSum) // 在自定义curry
//接受全部参数
console.log(curried(1,2,3))

// 接受部分参数,返回新的一个函数,等待接收剩余的参数
console.log(caches(1)(2,3))

console.log(curried(1,2)(3))

function curry(func){
    return function curriedFn(...args) {
        //判断实参和形参的个数
        if(args.length < func.length){
            return function () {
                //合并
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 实参和形参个数相同,调用 func,返回结果
        return func(...args)
    }
}

柯里化总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种函数参数的程序
  • 让函数变得更令化,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功效