这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
函数式编程(Function Programming,FP),FP是编程范式之一,我们常听的编程范式还有面向对象过程编程、面向对象编程。
-
面向对象编程的思维方式
把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
-
函数式编程的思维方式
把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 程序的本质:根据输入通过某种运算得到相应的输出,程序开发过程中会涉及很多有输入和输出的函数
- 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sine(x),x和y的关系
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射
优点:
- 可以让代码重用
- 在函数式的过程中,我们抽象出来的函数都是细粒度的函数,这些函数我们可以组合成功能更强大的函数
//非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
//函数式
function add(num1,num2){
return num1 + num2
}
let sum = add(2,3)
console.log(sum)
函数是一等公民 First-class Function
-
函数可以存储在变量中
//把函数赋值为变量 //函数表达式 let fn = function () { console.log("hello 函数一等公民") } fn() //一个示例 const BlogController = { /* 如果一个函数包裹另一个函数,并且形式也相同时,我们可以认为是一个函数 是把Views.index赋值给index,是一个函数赋值给index,是方法本身,而不是返回值所以要把方法调用去掉 */ index (posts) { return Views.index(posts) }, show (posts) { return Views.show(posts) }, create (posts) { return Views.create(posts) }, update (posts) { return Views.update(posts) }, destroy (posts) { return Views.destroy(posts) }, } //优化 //把一个方法赋值给另一个方法或者函数 const BlogController = { index : Views.index, show : Views.show, create : Views.create, update : Views.update, destroy : Views.destroy, } -
函数可以作为参数
-
函数可以作为返回值
在javaScript中函数就是一个普通的对象(可以通过new Funciton()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new Function('alert(1)')来构造一个新的函数
高阶函数 Hight-order Function
- 可以把函数作为参数传递给另一个函数 函数作为参数可以让函数更为灵活,调用foreach不用考虑内部的实现方式
//foreach是遍历数组的每一个元素,然后对每一个元素进行处理
function forEach(array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
//测试
let arr =[1,3,5,4,9]
forEach(arr,function(item){
console.log(item)
})
//filter过滤数组中满足条件的元素
function filter(array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
//测试
let arr = [1, 3, 5, 4, 9]
let r = filter(arr, function (item) {
return item % 2 === 0
})
console.log(r)
- 可以把函数作为另一个函数的返回结果
function makeFn() {
let msg = 'make funciton'
return function () {
console.log(msg)
}
}
// 用来接收makeFn返回的函数
let fn = makeFn()
fn()
//第一个()表示执行makeFn再加一个()表示执行makeFn返回的函数
makeFn()()
//once
//支付的时候不管用户点击多少次,函数只执行一次
function once(fn) {
//done来记录fn是否被执行
let done = false
return function () {
//fn还未被执行
if (!done) {
done = true
// 通过apply来调用fn,arguments表示调用当前函数的参数
//apply使用会立即执行,这一点是和bind的区别
return fn.apply(this, arguments)
}
}
}
let pay = once(function(money){
console.log(`支付:${money}RMB`)
})
pay(5) //支付:5RMB
pay(5)
pay(5)
pay(5)
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注我们的目标
- 高阶函数是用来抽象通用的问题
- 使代码更简洁
- 可以使代码更灵活
//面向过程的方式
//需要关注细节
let array =[1,2,4,6,7]
for(let i=0; i<array.length;i++){
console.log(array[i])
}
//高阶函数
let array =[1,2,4,6,7]
//不需要关注循环变量的控制
forEach( array,item => {
console.log(item)
}
//过滤数组中的元素,过滤的条件通过传入的条件来决定,不需要关注内部实现的细节,使代码更简洁
let r = filter(array,item => {
return item % 2 === 0
})
常用的高阶函数
- forEach、filter
//高阶函数-函数作为参数
//foreach是遍历数组的每一个元素,然后对每一个元素进行处理
function forEach(array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
//测试
let arr =[1,3,5,4,9]
forEach(arr,function(item){
console.log(item)
})
//filter过滤数组中满足条件的元素
function filter(array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
//测试
let arr = [1, 3, 5, 4, 9]
let r = filter(arr, function (item) {
return item % 2 === 0
})
console.log(r)
- map、every、some
//map array some
/*map
是对数组中的每一个元素进行遍历,
并对每一个元素进行处理,并且把结果返回到一个新的数组中
*/
const map = (array, fn) => {
const results = []
// for of是对for的抽象
for (let value of array) {
results.push(fn(value))
}
return results
}
/*
总结:map函数的参数是一个函数,是一个高阶函数,
可以通过指定函数对数组中的元素进行任意的求值,
函数参数会让我们的map函数更灵活
*/
let arr = [1, 5, 7, 9, 12]
arr = map(arr, v => v * v)
console.log(arr)
/*every
用来判断数组中的元素是否都匹配我们指定的一个条件
这个条件是灵活的,是变化的
*/
const every = (array, fn) => {
let result = true
for (let value of array) {
result = fn(value)
if (!result) {
break
}
}
return result
}
// 测试
let arr = [1, 5, 7, 9, 12]
let result = every(arr, v => v > 0)
console.log(result)
/*
some 与every类似,来检测我们数组中的元素是否有一个满足我们的条件
*/
const some = (array, fn) => {
let result = false
for (let value of array) {
result = fn(value)
if (result) {
break
}
}
return result
}
// 测试
let arr = [1, 5, 7, 9, 11]
let r = some(arr, v => v % 2 === 0)
console.log(r)
数组基础内容
数组创建
使用字面量表示法创建数组不会调用Array构造函数 Es6中新增的用于创建数组的静态方法:from()和of()
- from用于将类数组结构转换为数组实例
- of用于将一组参数转换为数组实例
/*Array.from()
用于将类数组结构转换为数组实例
*/
console.log(Array.from('12345'));
//对数组进行浅复制
const a1 = [1, 4, 5, 7]
const a2 = Array.from(a1)
console.log(a2)//[1,4,5,7]
console.log(a1 == a2)//false
//可以使用任务可迭代对象
//arguments可以轻松的被转为数组
function getArgsArray() {
return Array.from(arguments)
}
console.log(getArgsArray(1, 3, 4, 5))//[ 1, 3, 4, 5 ]
//也可用of来实现,用来替换es6之前常用的Array.prototype.slice.call(arguments)这种将参数转为数组的写法
console.log(Array.of(1, 3, 4, 5))//[ 1, 3, 4, 5 ]
//from也能转换带有必要属性的自定义对象
const arrayLikeObject = {
0: 1,
1: 5,
2: 7,
1: 0,
'length': 6
}
console.log(Array.from(arrayLikeObject))
/*[ 1, 0, 7, undefined, undefined, undefined ]*/
//from()还可以接受第二个可选的映射函数参数,这个函数可以增强新数组的值,还可以接受第三个可选参数,用于指定映射函数中的this,但这个重写的this值在箭头函数中不适应
const b1 = [1, 2, 4, 5]
const b2 = Array.from(b1, x => x ** 2)
const b3 = Array.from(b1, function (x) {
return x ** this.exponent
}, {
exponent: 2
})
console.log(b2)//[ 1, 4, 16, 25 ]
console.log(b3)//[ 1, 4, 16, 25 ]
数组空位
es6与前期版本略有不同,es6新增方法普遍将这些空位当成存在的元素,只不过值为undefined
const options = [1,,,,5]
for(const i of options){
console.log(option === undefined )
}
/*'
false
true
true
true
true
false
*/
es6之前的方法则会忽略这个空位,但具体方法也会因方法而异
let options = [1,,,,5]
//map会跳过这个空位
console.log(options.map((item)=>6))//[6,,,,6]
//join视空位置为空字符串
console.log(options.join('-'))//"1----5"
由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位,若确实需要,则可以显示的用undefined代替
数组索引
length属性不是只读的,通过修改可以从数组末尾删除或者添加元素
let colors = ['red','blue','green']
colors.length = 2
console.log(colors[2])//undefined
数组最多可以包含4 294 967 295个元素 约43亿
数组检测
只有一个全局作用域的情况下使用instanceof操作符足以检测一个对象是不是数组,
不确定在哪个全局上下文中创建时可以使用Array.isArray()
数组迭代器方法
Array数组原型暴露了3个用于检索数组内容的方法
- keys()
- values()
- entries()
let arr1 = ['foo','bar','baz','qux']
/*
这些方法都返回迭代器,可以将他们的内容通过Arrayf.from()直接转化为数组实例
*/
console.log(Array.from(arr1.keys()))//[0, 1, 2, 3]
console.log(Array.from(arr1.values()))//['foo','bar','baz','qux']
console.log(Array.from(arr1.entries()))//[[0,'foo'],[1,'bar'],[2,'baz'],[3,'qux']]
闭包
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
核心作用:延长了外部函数的内部变量的作用范围
//函数作为返回值
function makefn(){
let msg = `hello funciton`
return function (){
console.log(msg)
}
}
/*
*/
const fn = makefn()
fn()
/*
once
*/
function once(fn){
let done =false
return funciton(){
if(!done){
done=true
return fn.apply(this,arguments)
}
}
}
let pay = once(funciton (money)) {
console.log(`支付:${money}RMB`)}
//只会支付一次
pay(5)
pay(5)
pay(5)
pay(5)
闭包的本质:函数执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能被释放,因此内部函数依然可以访问外部函数的成员
纯函数
相同的输入始终会得到相同的输出,没有任何可观察的副作用
- 纯函数就是类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
- lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
- 数组的slice和splice分别是:纯函数和不纯的函数
- slice返回数组中的指定部分,不会改变原数组
- splice对数组进行操作返回该数组,会改变原数组(包括删除、修改)
//纯函数和不纯函数
let array = [1, 2, 5, 8, 0]
//纯函数是指相同的输入始终有相同的输出
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
//[1,2,5]
//splice是不纯的函数
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
//[1,2,5] [8,0] []
- 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
- 我们可以把一个函数的执行结果交给另一个函数去处理
loadsh纯函数库
//演示lodash
// first last toUpper reverse each
// es6中新的属性includes find findIndex
const _ = require('lodash')
const array = ['jack', 'tom', 'lucy', 'kate']
console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.first(array)))
//数组的reverse没有参数,不是一个纯函数
console.log(array.reverse(), 'array.reverse')
console.log(_.reverse(array))
// each是forEach的别名
const r = _.each(array, (item, index) => {
console.log(item, index)
})
console.log(r)
console.log(array.includes(1), 'array.includes(1)')
console.log(array.find(item => item.length > 3), 'array.find(1)')
console.log(array.findIndex(item => item.length > 3), 'array.findIndex(1)')
这里的练习主要涉及了数组的搜索和位置方法 ECMAScript提供了两类搜索数组的方法:按严格相等搜索和按断言函数搜索
严格相等
- indexOf()
- lastIndexOf()
- includes() 为es7中新增的
这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置,lastIndexOf第二个参数表示最后一个元素的位置
indexOf和incudes方法是从前往后搜索,lastIndexOf是从数组末尾开始向前搜索,但是lastIndexOf返回的位置与indexOf一致 indexof和indexLastOf都返回要查找的元素在数组中的位置,如果没有则返回-1 includes返回布尔值,表示是否找到一个与指定元素匹配的项
在比较第一个参数跟数组每一个项时,会使用===比较,也就是说两项必须严格相等
let num = [1, 2, 3, 4, 5, 4, 3, 2, 1, 6]
console.log(num.indexOf(4))//3
console.log(num.lastIndexOf(6))//9
console.log(num.includes(4))//true
console.log(num.indexOf(3,2))//true
console.log(num.lastIndexOf(3,4))//true
let person = { name: 'nike' }
let people = [{ name: 'nike' }]
let morePeople = [person]
console.log(people.indexOf(person))
console.log(morePeople.indexOf(person))
console.log(people.includes(person))
console.log(morePeople.includes(person))
断言函数
断言函数接收三个参数:元素、索引和数组本身 find()和fineIndex()都从数组的最小索引开始,find返回第一个匹配的元素,findIndex返回第一个匹配元素的索引,都可接收第二个可选参数,用于指定内部this的值,找到匹配项后,这两个方法都不再继续思索。
//断言函数
const people = [
{
name: 'Matt',
age: 28
}, {
name: 'nike',
age: 30
}
]
//find返回第一个匹配的元素,findIndex返回第一个匹配元素的索引
console.log(people.find(item => item.age > 28))
console.log(people.findIndex(item => item.age > 28))
//找到匹配项后,这两个方法都不再继续搜索。
let nums = [3, 6, 9]
nums.find((item, index, array) => {
console.log(item)
console.log(index)
console.log(array)
return item > 1
})
纯函数的好处
- 可缓存
- 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来,主要是用来提高性能
//记忆函数
const _ = require('lodash')
function getArea(r) {
console.log(r)
return Math.PI * r * r
}
// 会返回一个带有记忆功能的函数
// let getAreaWithMemory = _.memoize(getArea)
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
//模拟memoize实现
function memoize(fn) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
- 可测试 纯函数让测试更方便
- 并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任务运行纯函数(es6以后新增的web Worker,可以开启多个线程)
副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
//不纯函数
//函数依赖于外部的状态就无法保证输出相同
let mini = 18 //带来了副作用
function checkAge () {
return age >= mini
}
//纯函数(有硬编码,后续可以通过柯里化解决)
function checkAge () {
let mini = 18//是一个具体的数字,硬编码,要尽量避免
return age > = mini
}
副作用让一个函数变的不纯(如上例),纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用.
副作用来源:
- 全局变量
- 配置文件
- 数据库
- 获取用户的输入
- ......
所有的外部的交互都有可能产生副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患(例如我们获取用户输入时可能会带来跨站脚本攻击),给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。
柯里化(Haskell Brooks Curry)
//函数的柯里化
function checkAgeB(min) {
return function (age) {
return age >= min
}
}
//es6箭头函数
let checkAgeB = min => (age => age >= min)
let checkAgeB18 = checkAgeB(18)
let checkAgeB20 = checkAgeB(20)
console.log(checkAgeB18(25))
console.log(checkAgeB18(20))
console.log(checkAgeB20(20))
console.log(checkAgeB20(24))
柯里化:
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余的参数,返回结果
lodash中的柯里化
//lodash中的curry基本使用
const _ = require('lodash')
//柯里化可以帮我们把任意多元函数转为一元函数
function getSum(a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
//如果传入了getSum所需要的所有参数,则会被立即调用并返回结果
console.log(curried(1,2,3))
//如果传入参数为部分参数,它会返回一个函数,并且等待接收其他的参数
console.log(curried(1)(2,3))
console.log(curried(1)(2)(3))
柯里化案例
//柯里化案例
const _ = require('lodash')
//匹配字符串中的所有字符
// ''.match(/\s+/g)
//匹配字符串中的所有数字
// ''.match(/\d+/g)
//函数式编程可以最大程度的重用函数
// function match(reg,str){
// return str.match(reg)
// }
//match柯里化处理
let match = _.curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNum = match(/\d+/g)
console.log(haveSpace('Hello Sine ya'))
console.log(haveNum('Hello2 Sine4 ya1'))
//寻找数组中所有含有空格的字符串
const filter = _.curry(function (fn, arr) {
return arr.filter(fn)
})
const filter = _.curry((fn, arr) => {
return arr.filter(fn)
})
const findSpace = filter(haveSpace)
console.log(filter(haveSpace, ['qw e', 'rf p', 'ert']))
console.log(findSpace(['oi p', 'ioj']))
柯里化的实现
//模拟实现lodash中的curry方法
const _ = require('lodash')
function getSum(a,b,c) {
return a + b + c
}
/*
调用_.curry时我们要传入一个纯函数
返回一个柯里化后的函数
*/
// const curied = _.curry(getSum)
const curied = curry(getSum)
console.log(curied(1,2,3))
console.log(curied(1,2)(3))
console.log(curied(1)(2)(3))
//需要一个经过柯里化处理的函数
function curry(fn){
return function curriedFn(...args){
//第一种情况就是这个柯里化函数需要几个函数我们就传入几个函数
//第二种,调用curry函数时只传入部分参数,返回一个等待接收其他参数的函数
//我们要获取一下形参的个数是否与fn实参的个数
if(args.length<fn.length){
return function(){
// arguments是伪数组所以要用Array.from进行处理
return curriedFn(...args.concat(Array.from(arguments)))
}
}else{
return fn(...args)
}
}
}
柯里化总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的缓存
- 让函数变的更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生从强大的功能
函数的组合Compose
-
纯函数和柯里化很容易写出洋葱代码h(g(f(x)))
- 获取数组的最后一个元素再转换成大写字母_.toUpper(.first(.reverse(arr)))
- 函数组合可以让我们把细粒度的函数重新组合成一个新的函数
-
如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
//函数组合演示
function compose(f,g){
return function(value){
return f(g(value))
}
}
//翻转
function reverse(arr){
return arr.reverse()
}
//数组的第一个元素
function first(arr){
return arr[0]
}
const last = compose(first,reverse)
console.log(last([2,3,5,6]))
lodash组合函数
-flow()从左到右执行 -flowRight() 从右到左执行,过程中用的比较多
//lodash中的组合函数 _.flowRight
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = _.flowRight(toUpper,first,reverse)
console.log(f(['we','rt4t','rer']))
模拟lodash的flowRight
//lodash中的组合函数 _.flowRight
const _ = require('lodash')
const { CodeNode } = require('source-list-map')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// const f = _.flowRight(toUpper,first,reverse)
// function compose(...args){
// return function(value){
// //对数组中的每一个元素去执行我们提供的一个函数,并将其汇总成一个单个的结果
//acc 表示上一次的执行结果,fn表示当前管道
// return args.reverse().reduce(function(acc,fn){
// return fn(acc)
// },value)
// }
// }
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse)
console.log(f(['we', 'rt4t', 'rer']))
函数的组合要满足结合律(associaltivity)
- 我们既可以把g和h组合,还可以把f和g组合,结果都是一样的
let f = compose(f,g,h)
let associaltive = compose(compose(f,g),h) == compose(f,compose(g,h))
//true
//函数组合要满足结合律
const _ = require('lodash')
// const f = _.flowRight(_.toUpper,_.first,_.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse))
console.log(f(['we', 'rt4t', 'rer']))
如何调试组合函数
const f = _.flowRight(_.toUpper,_.first,_.reverse)
//函数组合调试
//NEVER SAY DIE --> never-say-die
const _ = require('lodash')
// const log=(c)=>{
// console.log(c)
// return c
// }
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
//_.split
const split = _.curry((sep, str) => _.split(str, sep))
//_.toLower
const map = _.curry((fn, arr) => _.map(arr, fn))
//_.join
const join = _.curry((seq, arr) => _.join(arr, seq))
const result = _.flowRight(join('-'),trace('map之后打印'), map(_.toLower),trace('split之后打印'), split(' '))
console.log(result('NEVER SAY DIE'))
losash中的FP模块
- lodash的fp模块提供了使用的对函数式编程友好的方法
- 提供了不可变auto-curried iteratee-first data-last的方法(是已经被柯里化的,如果是函数的话会要求函数优先,并且数据滞后)
//lodash 模块
//数据优先,函数滞后
_.map(['a','b','c'],_.toUpper)
//=>["A","B","C"]
_.map(['a','b','c'])
//['a','b','c']
_.split("Hello World",' ')
//lodash/fp 模块
//函数优先数据滞后
const fp = require('lodash/fp')
fp.map(fp.toUpper,['a','b','c'])
fp.map(fp.toUpper)(['a','b','c'])
fp.split(' ',"Hello World")
fp.split(' ')("Hello World")
// NEVER SAY DIE --> never-say-die
const fp = require('lodash/fp')
let f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
console.log(f('NEVER SAY DIE'))
point Free 函数编码的风格
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// point free
// Hello World => hello_world
const fp = require('lodash/fp')
const pf = fp.flowRight(fp.replace(/\s+/g,"_"),fp.toLower)
console.log(pf("Hello World"))