以对话的方式——让我们一起进入js函数式编程的世界(上)

1,099 阅读27分钟

使用对话的方式——将你带入函数式编程的世界

写在前面:本次函数式编程的文章小白分为了两篇,本篇主要是初步和感知函数式编程的魅力,下篇则是函子及其对函数式编程的其他知识点的补充

还是:如有误,望指教 。感恩!

临渊羡鱼,不如退而结网

7月中旬的时候,闲着无聊无聊刷到了尤大的微博,得知VUE3.0 RC出来了。

看到了这个消息,小白暗暗苦笑。知道又来活了,VUE 3.0 相比于2.x来说改动确实挺大的,尤其是那个被津津乐道的composition API,小白搓了搓手心想到又可以大干一场了。

然而小白在找寻相关资料的时候,被另一个神奇且牛x的东西吸引住了,小白的眼神竟然迟迟不能移动。脑子里不觉般忘记了什么vue 3.0,什么composition API,此刻死死地印在小白脑海的只有眼前那五个大字。“函数式编程”

一: 认识函数式编程

1.1 什么是函数式编程

小黑: 呸 ,你就是个渣男。这个所谓的“函数式编程是什么来历”?

小白:嗯...,所谓函数式编程嘛,其实就是一种编程范式,也就是一种编写代码的方式。我们常用的编程范式现在一般就是面向对象、面向过程了这些。

面向对象的思维方式我们都很熟悉,就是把现实世界的事物抽象成程序世界的类和对象,然后再通过继承、封装、多态来演示事物之间的关系

函数式编程的思维模式,就是它强调了以函数使用为主的开发风格。它的主要思想是把运算过程尽量写成一系列嵌套的函数调用。类似于数学中的y=f(x)

再具体点呢。在函数式编程中所有的函数都是要有输入和输出的,对于这样一个数学公式y=tan(sinx)来说,y相当于我们这个程序输出结果,x想当于我们数据的输出。那么在这个程序中x先会传给sin这个函数,经过sin函数返回一个结果又被当成入参传给tan函数,最终得到结果y

小黑:能不能举个程序上的栗子呢?

小白:好勒,比如我们设计这么一个简单的程序,计算(2+3)*4

普通的面向过程的写法是这样的

let a = 2
let b = 3
let c = 4

console.log((a + b) * c)//20

使用函数式编程的写法是这样的

let a = 2
let b = 3
let c = 4
function add(x, y) {
    return x + y
}
function multiply(x,y){
    return x*y
}
console.log(multiply(add(a,b),c))//20

从这样一个简单的栗子来说,可能函数式编程的写法显得更加复杂了。可是不要忘了函数是可以复用的啊。multiply(add(a,b),c)这种嵌套的调用方式确实不太好看,改进的方法还得请您继续往下看

1.2 函数式编程的好处

小黑:既然这个函数式编程这么的吸引你,它有什么优点呢?

小白:让我想想怎么给你说啊,毕竟优点太多了,我就先捡着重点来咯

  1. 函数式编程因为大量使用函数,故可大大减少代码的重复性问题

  2. 函数式编程中,一个函数不依赖、也不会去改变它外界的状态。只要给它特定的输入,它就是返回给你特定的输出。极其方便单元测试

  3. 代码的可读性强

先讲这几个在本文中最容易体现的吧

二:走进函数式世界

本章节我们主要来看一下在函数式编程中的几个重要知识点。

2.1 函数是一等公民

小黑:经常听到有人说这句话,“函数是一等公民、函数是一等公民”,这个到底是什么意思呢?函数怎么就成了一等公民了呢?

小白:其实这也就是这么一种说法,小白也不知道为啥要搞这么一个一等公民的说法。我们都知道在js中,函数除了做它函数的角色实现某种功能之外,它本身可就是一个对象。既然是对象那么就和普通的变量一样,可以它放进数组、可当做参数传递、可当做返回值、可赋值给另一个变量。我想之所以称呼它是一等公民,是因为它同时拥有函数、变量这两种角色吧。

函数式编程为何钟爱一等公民

小黑:听说函数式编程是非常钟爱于一等公民,这是怎么一回事呢?

小白:这就得展示几个栗子来看咯(还是来看 Franklin Risby 教授 在函数式编程指北的栗子吧)

栗子1

const hi = name => `Hi ${name}`;
const greeting = name => hi(name);

我可不信没人写过这样的代码,反正小白想了一下,这种代码小白是写过不少的。

同时也该庆幸,幸亏小白的代码不会被人发现,不然被暴躁的 Franklin Risby 教授喷上一喷,这谁能顶的住啊

不过也确实上面那两行代码现在看来属实憨憨了。因为greeting的作用本来就是将hi函数执行一下,那么按照函数是一等公民,我们完全可以把hi函数当成变量赋值给greeting变量啊

改进一下吧

const hi =name=>`HI ${name}`
const greeting=hi

栗子2

const BlogController = {
  index(posts) { return Views.index(posts); },
  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); },
};

小白温馨提醒一下,这段代码要仔细的看一下哟,这段代码可是直接被教授称之为垃圾的代码

我们来仔细分析一下吧,这个控制器对象中有5个方法,我们单拎出来一个

index(posts) { return Views.index(posts); },是不是有点跟栗子1那段代码有点眼熟了,既然index函数和Views.index函数的参数是一样的,且index函数的执行结果也就是Views.index函数的执行结果。嗯...这不就是一个人吗,是不是又有了一丝脱裤子放屁的味了

即这行代码我们也完全可以按照函数是一等公民的特性,直接将Views.index函数当成变量赋值给index

像这种:

const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

代码一下子变的清晰无比了有木有!!!

2.2 高阶函数

小黑:高阶函数我知道,它就是说一个函数的参数如果也是一个函数、或者一个函数的返回值也是一个函数,那么这个函数就被叫做高阶函数。这个我们都明白,给我们搞点栗子瞅一瞅吧

小白:遵命老大!回想js中所遇到过的高阶函数最多最多的就是es5中,关于数组那块了吧。那我们就来看看它们的使用以及实现吧

注意:这里小白主要注重用法与实现,更加具体的请参考MDN

常见高阶函数及其实现

forEach

对数组的每个元素执行一次给定的函数。

//示例:
[1, 2, 3].forEach(function(value) { console.log(value) })

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

forEach([123], function(value) {
    console.log(value);
})
filter

创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

//示例
let arr=[12345].filter(function(value){
    return value>3
})
console.log(arr)//[ 4, 5 ]

//实现
function filter(arr, fn) {
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (fn(arr[i])) {
            res.push(arr[i])
        }
    }
    return res
}

let res = filter([12345], function(value) {
    return value > 3
})
console.log(res)
map

创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

//示例
let arr = [1234].map(function(value) {
    return value + 1
})
console.log(arr)//[ 2, 3, 4, 5 ]

//实现
function map(arr, fn) {
    let res = []
    for (let i = 0; i < arr.length; i++) {
        res.push(fn(arr[i]))
    }
    return res
}

let res = map([1234], function(value) {
    return value + 1
})
console.log(res)
every

测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。 即全部通过才返回true,有一个没有通过就是false

//示例
let res=[12345].every(function(value){return value<6})
console.log(res)//true

//实现
function every(arr, fn) {
    let res = true
    for (let i = 0; i < arr.length; i++) {
        res = fn(arr[i])
        if (!res) {
            break
        }
    }
    return res
}

let res = every([12345], function(value) { return value < 6 })
console.log(res)
some

测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。 即有一个通过就返回true,全没有通过就是false

//示例
let res=[1234].some(function(value){return value>4})
console.log(res)//false

//实现
function some(arr, fn) {
    let res = false
    for (let i = 0; i < arr.length; i++) {
        res = fn(arr[i])
        if (res) {
            break
        }
    }
    return res
}

let res = some([1234], function(value) { return value > 4 })
console.log(res)

reduce

对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。 就是一个数组元组的求和函数呗

//示例
let sum = [1234].reduce(function(pre, next) {
    return next + pre
})
console.log(sum)//10

//实现
function reduce(arr, fn, initVal) {
    let res = initVal === undefined ? arr[0] : initVal
    let i = initVal === undefined ? 1 : 0
    for (i; i < arr.length; i++) {
        res = fn(res, arr[i])
    }
    return res
}
console.log(reduce([1234], function(pre, next) {
    return next + pre
}))

高阶函数的好处

小黑:这种高阶函数的存在有什么好处呢?

小白:这您可是问到点子上了,小白也想知道呢。接下来小白就按照自己理解的简单的说一下吧

先来讲参数是函数的:

这个比较好明白,因为我们上面实现的几个栗子都是这种形式的。它的好处已经非常显而易见吧,它能非常自由灵活的处理我们函数中的数据

再来讲返回值的函数的:

一个函数的返回值是一个函数?这句话不知道能不能使你想起点什么,反正小白一看到这句话马上浮现在脑海的就是闭包。闭包的所有好处是不是这里也就拥有了呐

👉关于闭包小白原来也写过一个总结

2.3 纯函数

小黑:前几个东西,俺老黑可能有些模糊。可这个什么纯函数俺是真的没有听说过啊,你可要给俺好好讲讲。

小白:黑哥,难得宁这么有礼貌的跟我说话。

什么是纯函数

首先我们先来看它的概念:所谓纯函数它是这么一种函数,给予相同的输入,总是会得到相同的输出,而且没有任何可观察的副作用

什么意思呢?

还是来看一个栗子,比如我们给一个网吧写一个检查年龄的函数,年龄小于所设的基准值就不让他进去

//不纯
let min = 18

function checkAge(age) {
    return age > min
}
//纯
function checkAge(age, min) {
    return age > min
}

小黑:为啥上面的就不纯了呢?

小白:上面的函数它引入了外部的环境,而外部的东西是不可预测的。也就是说这个函数除了输入值之外还有东西能够影响到它的输出。这个栗子可能不能完全匹配纯函数的概念,我们再来看一下数组中的两个API =>slice、splice

这两个API都可以将一个长数组缩短为短数组,不同的是一个会改变原数组一个不会改变。看代码

const arr01 = [123456]
const arr02 = [123456]


let res01 = arr01.slice(03)
let res02 = arr01.slice(03)
let res03 = arr01.slice(03)

console.log(res01, res02, res03)//[ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ]

let res04 = arr02.splice(02)
let res05 = arr02.splice(02)
let res06 = arr02.splice(02)
console.log(res04, res05, res06)//[ 1, 2 ] [ 3, 4 ] [ 5, 6 ]

可以看到,对于slice来说相同的输入总会得到相同的输出,所有它就是纯函数;对于splice来说相同的输入也得不到相同的输出,所以它是一个不纯的函数

总结一下吧:纯函数就像一个与世无争的隐士,它只会老老实实的处理你交给它的数据。除此之外它不受外界的干扰,也不会影响外界元素。

这不正是我们最为喜爱的吗,鬼知道小白刚踏入前端的时候,在数组和对象这种引用类型上踩了多少坑,那时年少无知的小白因为不熟悉API,每次改造数组时经常把数据改的乱七八糟,最后一检查,喔开始原数组就被小白整的面目全非了

什么是副作用

再来看一下眼纯函数的概念:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

小黑:前半截老黑明白了,这个副作用指的是啥嘞?

小白:你可以这样理解,所谓函数的副作用:就是这个函数除了返回了一个值之外,这小子还偷偷的干了一些其他不好的事情。如

  • 可能修改了外界的元素
  • 抛了一个异常或者直接错误终止
  • 读取或写入了一个文件
  • 操作了DOM
  • ...

总之一句话,只要这个函数内部跟外界的环境发生了交互就都是副作用。

小黑:这样也忒严格了吧,这样搞哪还有几个没有副作用的呦

小白:黑哥听我把话说完呀,函数式编程的哲学就是它把副作用当成一个函数造成不当行为的主要原因,这可并不是说,就必须要禁止一切副作用。

而是要让这些副作用在可控制的范围内发生。后面的函子就是用来控制他们的。

再总结一下:一个函数需要跟他外部环境打交道(如splice),那么就无法保证对于这个函数相同的输入会返回相同的输出,那么就不能保证它是纯函数了

纯函数可是个世外高人,不问外界是非!!!

三: 柯里化

3.1 简单认识

小黑:柯里化,喔,这个我知道。我见过不少有关柯里化的面试题了

小白:嗯...,确实。我开始接触它同样是因为那些面试题,可是那时我一点都不知道它的用处,只是为了刷题而刷题

直到接触到到了函数式编程,我才真正的感受到了柯里化的魅力。

先来回顾一下柯里化的概念:一个函数,只传递一部分参数去调用它,让它返回一个新的函数去处理剩下的参数

来看一下我们上面写过的一个栗子,用它来体会一下柯里化带来的好处吧

function checkAge(min, age) {
    return age > min
}

假设这个网吧只允许18岁以上的人进入,这时来了几个人。我们就得这样调用方法进行检查

checkAge(17,18)//fasle,不可进入
checkAge(27,18)//true,可进入
checkAge(20,18)//true,可进入

每一次调用都得带上这个基准值18,是不是很恶心呢?

来我们给这个函数柯里化一下:

借用一下lodash中的柯里化函数curry(安装lodash:npm i lodash)

const _ = require('lodash')

function checkAge(min, age) {
    return age > min
}
const fn = _.curry(checkAge)
const f = fn(18)

f(17)//false
f(27)//true
f(20)//true

这样不是好多了嘛

3.2 lodash中的柯里化函数

小黑:你再用lodash中的这个curry函数给我老黑举个栗子呗,就上面一个栗子俺咋能看的明白啊

小白:来咯来咯

再来看这个函数:

function getSum(ab, c) {
    return a + b + c
}

我们用curry将它柯里化吧

const _ = require('lodash')
const fn = _.curry(getSum)
console.log(fn(12)(3))//先传两个参,返回一个函数处理第三的参
console.log(fn(1)(23))//先传两个参,返回一个函数处理第三的参
console.log(fn(123))//参数一次性传完了,直接执行结果
console.log(fn(1)(2)(3))//先传1个参,返回一个函数处理第剩下的,传入第二个参则还是返回一个函数处理剩下的,直到参数全部传入则执行结果

3.3 实现curry函数

小黑:哇,这个curry函数看起来好屌的样子喔。小白快给我写一个!

小白:yes sir

先看一下curry函数的使用,它接收一个函数作为参数,又返回一个函数。

其中返回的函数里面得有一个对参数长度的判断,以便知道啥时候调用开始传入的参数函数

开写:

function curry(fn) {
    return function middle(...args) {
        return args.length >= fn.length ? fn(...args) : function(...arg) {
            return middle(...arg, ...args)
        }
    }
}

简单介绍一下吧:首先curry函数返回的这个函数是需要接收参数的,参数的个数不确定,但是需要和fn函数参数个数做对比。如若大于等于fn函数的参数个数,直接调用fn并把middle函数接收的参数传进去。之所以要给curry函数调用返回的函数取名字,相信你也想到了。因为只要参数没有到达fn参数的长度,就得不断的返回一个新的函数。递归呗

可能光return看着不爽,改进一步咯

const curry fn =>
    middle = (...args) => args.length >= fn.length ? fn(...args) :(...arg) => middle(...arg, ...args)

总结一下呗:(这里可能还体会不到,慢慢往下看你就能体会到它的用途和重要性了)

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了固定参数的新函数

  • 这是一种对函数参数的缓存

  • 可以让函数变得更加灵活,让函数的粒度更小

  • 可以将多元函数转成一元函数,可以组合使用函数产生强大的功能

四:函数组合

4.1 开始认识

小黑:你开始不是说会改进multiply(add(a,b),c)这个恶心的洋葱一样的代码调用方式吗?你不会又要偷懒嘛?不会吧、不会吧

小白:当然不会,我这不正打算说了吗,你把我小白当成什么人啦

下面开始介绍函数组合,使用函数组合可以让我们把细粒度的函数重新生成一个新的函数

小黑:我要栗子,栗子,栗子!

小白:来了大哥~

这回我们换一个栗子,来写一个程序将数组的第一个元素中的字符串变成大写

改进前的写法

function getFirst(arr){
    return arr[0]
}
function toUpperCase(str){
    return str.toUpperCase()
}
const arr=['a','b']

//先获取一个元素,再把获得的这个元素当成入参传到toUpperCase
console.log(toUpperCase(getFirst(arr)))

好了,洋葱出来了。有一个console.log更洋葱了。。。

改进后:

function compose(f, g) {
    return function(arr) {
        return f(g(arr))
    }
}

function getFirst(arr) {
    return arr[0]
}

function toUpperCase(str) {
    return str.toUpperCase()
}
const arr = ['a''b']

const fn = compose(toUpperCase, getFirst)
console.log(fn(arr))

使用一个组合函数,将getFirst、toUpperCase组合成一个新的函数。这下可读性是不是一下子上来了,而且又多了一个可复用的fn函数,用于将数组第一个元素变为大小。复用性是不是也上来了

不过要注重给compose传参时的顺序,一般都是从右到左的(上面的compose是我们自己写所有可以随你便)。即先找到数组的一个元素,找到之后把它交给toUpperCase去转换成大小

4.2 lodash中的组合函数

小黑:是不是有一个像lodash中的curry函数一样的组合函数,可以帮助我们进行任意个数的函数组合呢?

小白:黑哥您真的是料事如神啊,什么都瞒不住你。lodash中也提供了两个关于函数组合的函数flow,flowRight。flow是它里面的参数函数执行顺序是从左到右;flowRight则是从右到左(这个常用)

举个栗子:将数组的最后一个元素变成大写(为举例我们直接获取数组的最后一个元素,而是先翻转数组再取它第一)

// 将数组的最后一个整成第一个变成大写
const _ = require('lodash')

function reverse(arr) {
    return arr.reverse()
}

function getFirst(arr) {
    return arr[0]
}

function toUpper(s) {
    return s.toUpperCase()
}
const f = _.flowRight(toUpper, getFirst, reverse)
console.log(f(['a''b''c']))

是不是贼好用呢

4.3 手写flowRight函数

小黑:这个flowRight这么牛x,小白你能不能...

小白:停停停,懂了黑哥。给你手写一个呗,看好咯

首先分析一下flowRight,它接收若干个函数作为参数,同时又返回一个函数。里面注意的就是多个函数的顺序问题

返回的这个函数是要接收实际的数据的,它里面要做的工作就是把传进去的数据经过开始传入的若干个参数函数进行处理。就像一个工厂流水线一样,数据先给最右的参数函数处理,处理返回结果交给右边倒数第二个,这样循环下去。

看着思路你有没有想到一个我们上面刚刚实现过的API。reduce是不是又派上了用场

开始实现咯

function compose(...args) {
    return function(value) {
        return args.reverse().reduce((pre, next) => {
            return next(pre)
        }, value)
    }
}

4.4 函数组合满足结合律

小黑:满足结合律?这是什么意思,数学运算的那个结合律吗?

小白:没错

我还是自觉点,直接上栗子吧。还是拿这个将数组最后一个元素变为大写的举例

// 将数组的最后一个整成第一个变成大写
const _ = require('lodash')

function reverse(arr) {
    return arr.reverse()
}

function getFirst(arr) {
    return arr[0]
}

function toUpper(s) {
    return s.toUpperCase()
}
const f = _.flowRight(toUpper, getFirst, reverse)
console.log(f(['a''b''c']))

满足结合律是什么意思呢?即看函数f

const f1 = _.flowRight(_.flowRight(toUpper, getFirst), reverse)
const f2 = _.flowRight(toUpper, _.flowRight(getFirst, reverse))

像数学运算那样,可以先组合后两个也可以先组合前两个。最后生成的组合函数f1和f2 。就是就同一样函数(语义上的同一个函数啊,你判f1==f2肯定是不相等的,不是一块存取空间啊)

4.5 组合函数玩法进阶

小黑:你刚刚总结柯里化的不是说后面会用到吗,到这了没有用到吗?

小白:嘿,这不巧了黑哥。我正打算说嘞

不知道你注意到没有我们上面组合的哪些纯函数都是只有一个参数喔,那么多个参数的要怎么办呢?

这个时候柯里化就得过来帮忙了

举个栗子:把HELLO WORLD转成hello-world

这里我们不使用js中的API了,我们用一下lodash中一些方法。使用基本上和js中是一样的,只不过它们比较纯而已

这里我们要用到lodash中的三个API

  • split:这里需要传两个参 1.需要处理的数据 2.指定切割符
  • map:也这里也需要传两个参 1. 需要处理的数据 2. 处理函数
  • join:这里同样需要传两个参 1.需要处理的数据 2. 指定分隔符
const _ = require('lodash')

// 注意在函数组合中我们需要的只有一个参数的纯函数

//分割
const split = _.curry(function(sep, str) {
    return _.split(str, sep)
})
//小写
const map = _.curry(function(fn, arr) {
    return _.map(arr, fn)
})
//join

const join = _.curry(function(sep, arr) {
    return _.join(arr, sep)
})

const f = _.flowRight(join('-'),  map(_.toLower), split(' '))

console.log(f('HELLO WORLD'))

看到没有,我们使用柯里化将它们除需要处理的其他数据(流水线上的那些出口和入口)在函数组合时就搞进去了。

4.6 调试 debug

小黑:我想到了一个问题,这种函数组合将多个函数整合到一起了,出错了我们怎么调试呢?我怎么知道是哪块出错了呢?

小白:问的好

直接拿上面的代码做例子了

/**
 * 调试
 */
const _ = require('lodash')

// 注意在函数组合中我们需要的只有一个参数的纯函数


//分割
const split = _.curry(function(sep, str) {
    return _.split(str, sep)
})

//小写
const map = _.curry(function(fn, arr) {
    return _.map(arr, fn)
})

//join

const join = _.curry(function(sep, arr) {
    return _.join(arr, sep)
})

//调试函数
const log = v => {
    console.log(v)
    return v
}
const f = _.flowRight(join('-'), log, map(_.toLower), split(' '))


console.log(f('HELLO WORLD'))

如果出现了错误,我们可以写一个测试函数,拿它就像是一根电笔一样在函数组合时各种调整位置进行测试

4.7 lodash中的fp模块

小黑:虽然这种多参的函数可以由curry去处理一样,可是每一个函数都这样单独改造。俺老黑觉得很是麻烦啊。

小白:黑哥,不要怕。lodash其实也帮我们解决了

来看一下lodash中的fp模块

  • lodash中的fp模块提供了实用的对函数式编程友好的方法
  • 同时它是柯里化的lodash,并且与简单的柯里化不同的是,它也帮助我们调整了参数传递的顺序

👉详细点

小黑:改变了参数传递的顺序?这是什么意思?

小白:黑哥一看你就是刚才没有好好看代码,我们再把上面改造后的split方法拿过来

const split = _.curry(function(sep, str) {
    return _.split(str, sep)
})

const f = _.flowRight(join('-'),  map(_.toLower), split(' '))

看清楚了没有,我们可不是直接对 _.split直接进行的柯里化。为啥呢?

因为_.split的第一参是要处理的数据,第二次才是要指定切割符。

而柯里化之外,我们的实际需求是要先把分隔符参数传进去的,所有开始柯里化的时候不得不包装一个函数用于交换顺序

而fp模块函数参数的顺序是数据滞后,也就是说他其实已经将数据参数移到了后面。对于我们来说是不是就更舒服了呢?用fp模块的函数改造一下吧

const fp = require('lodash/fp')

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('HELLO WORLD'))//hello-world

4.8 Point Free模式

小黑:呦呵!又来一名词?这又是啥玩意

小白:嘿嘿,黑哥莫慌。其实我们上面函数组合写的那些代码都是point free 模式

还是先来看一下它的概念吧:point free 模式是指我我们可以把数据处理的过程定义成数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合到一起。

什么意思呢?还是搬一个上面的例子

const fp = require('lodash/fp')

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
f('HELLO WORLD')

看这个组合函数const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))里面是不是没有一点要处理的数据参数的影子呢

再来看一下非point free 模式的写法

const f = function(str) {
    return str.split(' ').map(value => value.toLowerCase()).join('-')
}
console.log(f('HELLO WORLD'))

对比point free和非point free,模式的两个函数。非point free中的f函数提到了数据参数str,而point free模式的f函数则没有这个数据参数,它仅是在f函数调用时传入的

Point Free:我们可以把数据处理的过程定义成数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合到一起,在使用这么模式之前我们需要定义一些辅助的基本运算函数

总结一下它的特点

  • 不需要指明处理的数据
  • 只需要定义一些辅助的基本运算函数进行合成

函数式编程的核心就是将运算过程抽象为函数,Point Free模式就是将抽象出来的函数们再抽象组合成一个新的函数

写到最后

本来是打算将函子一并总结完成的,但吸取了前几此写博客的经验。一次性篇幅太长小白就会感觉最后面的东西总结的便十分应付了,鉴于函子这块的知识(当然还有一些函数式编程的其他必知小知识)比较重要。所有暂时把他们放到后面的文章中了

参考致谢:

👉函数式编程指北

👉 JavaScript 函数式编程指南