js函数式编程01
什么是函数式编程
- 函数式编程,Functional Programming,简称FP,是一种编程范式,也是一种编程风格。
- 简单理解就是把事物与事物之间的联系抽象到程序世界
函数式编程和面向对象编程的不同
- 面向对象 是把事物本身进行抽象
- 函数式编程 是对运算过程的抽象
- 对于函数式编程的理解:
- 程序本质是通过某种运算获得响应的输出,而函数就是包括输入(参数)输出(返回值)
- 函数式编程这里的函数指的不是Function,把它理解为数学中的函数 y=f(x) 这种映射关系更加合理
高阶函数
高阶函数(Higher-order function)
- 函数作为参数,下面我们来封装一个类似于forEach的函数,其就是把函数作为参数
// 定义一个类似于js中数组方法forEach的函数
// 接收两个参数 一个是数组 另一个是数组中成员要执行的函数
function myForEach (array, fn) {
for(var i=0; i<array.length; i++ ){
fn(array[i])
}
}
// test
myForEach(['tom','jerry','jim'],item =>{ console.log(item) })
- 函数作为返回值
// 一个函数返回另一个函数
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
// test
// 第一种调用方式
const fn = makeFn()
fn() //Hello function
// 第二种调用方式
makeFn()()///Hello function
- 使用高阶函数的意义
抽象可以帮助我们屏蔽细节,高阶函数是用来抽象通用的问题
闭包
- 对于闭包的理解,首先要了解一点函数的局部变量存在栈中,当函数执行完就会被释放,但是闭包的产生是由于函数中局部变量被捕获了,导致无法释放,那么这个被捕获的局部变量为了保证不会被销毁,就会在堆中生成对象scope记录下来
- 所以闭包的本质就是,函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除。但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
// 闭包典型案例
for(var i=0; i<10; i++){
((j)=>{
setTimeout( ()=>{
console.log(j)
} )
})(i)
}
纯函数
纯函数其实就是数学中的函数概念 类似于 y=f(x),用来描述输入和输出的对应关系,是函数式编程重要的概念。
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。
let numbers = [1, 2, 3, 4, 5]
// 纯函数
// 对于相同的函数,输出是一样的
// slice方法,截取的时候返回截取的函数,不影响原数组
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
// 不纯的函数
// 对于相同的输入,输出是不一样的
// splice方法,返回原数组,改变原数组
numbers.splice(0, 3) // => [1, 2, 3]
numbers.splice(0, 3) // => [4, 5]
numbers.splice(0, 3) // => []
- 可以把一个函数的执行结果交给另一个函数处理
纯函数的好处
可缓存
因为对于相同的输入始终有相同的结果,那么可以把纯函数的结果缓存起来,可以提高性能
- 模拟lodash中的memoize函数,对纯函数的结果进行缓存
function memoize(f){
// 定义一个对象用于记录纯函数的参数和执行结果
let cache = {}
return function(){
// 将传入的参数作为对象的key
let key = JSON.stringfiy( arguments )
// 如果key有对应值说明是再次调用直接获取缓存中的值
cache[key] = cache[key] || f.apply(f,arguments)
return cache[key]
}
}
function getArea(r) {
console.log(r)
return Math.PI * r * r
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669
可测试
纯函数让测试更加的方便
并行处理
- 多线程环境下并行操作共享的内存数据很可能会出现意外情况。纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数
- 虽然JS是单线程,但是ES6以后有一个Web Worker,可以开启一个新线程
副作用
副作用就是让一个函数变得不纯,纯函数是相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同
比如纯函数中如果依赖于 全局变量进行计算 那么只要改变了全局变量 就会导致函数不纯
柯里化
当函数有多个参数的时候,我们可以对函数进行改造。我们可以调用一个函数,只传递部分的参数(这部分参数以后永远不变),然后让这个函数返回一个新的函数。新的函数传递剩余的参数,并且返回相应的结果
函数的柯里化可以帮助我们解决函数中的硬编码问题
// 定义一个checkAge函数
// 先传递比较年龄的min
function checkAge (min) {
return function (age) {
return age >= min
}
}
// 使用es6改写
let checkAge = min => (age => age >= min)
lodash 中的 curry() 方法
_.curry(func)
- 功能:创建一个函数,该函数接收一个或多个 func的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
const _ = require('lodash')
//match函数是根据一些正则,匹配字符串,返回匹配结果
const match = _.curry((reg, str) => str.match(reg))
//haveSpace函数是一个匹配空格的函数
const haveSpace = match(/\s+/g)
//haveNumber函数是一个匹配数字的函数
const haveNumber = match(/\d+/g)
//filter函数是定义一个数组和过滤规则,返回符合匹配规则的数组
const filter = _.curry((func, array) => array.filter(func))
//findSpace函数是匹配数组元素中有空格并返回符合情况的数组的函数
const findSpace = filter(haveSpace)
柯里化的好处就是我们可以最大程度的重用我们的函数
封装模拟lodash的curry柯里化转化方法
- 分析思路:
- 传入参数为纯函数func
- 返回值为一个柯里化之后的函数 curried
- 比较curried的实参个数…args 跟纯函数的形参个数func.length
- 如果实参个数大于等于形参个数 返回func的调用结果
- 如果实参个数小于形参个数 要再次返回一个新的curried ,并且指定其形参前几位为上一次curried调用时 传入的实参 ,并合并新的实参
function getSum(a, b, c) {
return a + b + c
}
function curry(func) {
return function curried(...args) {
// 函数的length属性获得函数形参个数
// es6的...args 函数的参数扩展 获取传入的不固定数量的实参
if (args.length < func.length) {
return function () {
// 第一部分参数在args里面,第二部分参数在arguments里面,要将两个合并并且展开传递
// concat函数要合并两个数组,arguments为伪数组,所以用Array.from进行转换
return curried(...args.concat(Array.from(arguments)))
}
}
// 如果实参个数大于等于形参个数 返回func的调用结果
return func(...args)
}
}
let s1=curry(getSum)
console.log(s1(1)(2)(3));
柯里化总结
- 柯里化可以让我们给一个函数传入较少的参数 得到一个已经记住了一部分参数的新函数
- 这是一种对函数参数的缓存
- 让函数变得更灵活,颗粒度更小
- 可以把多元函数编程一元函数,可以组合使用函数产生强大的功能