函数式编程中的函数指的不是程序中的函数方法,而是数学中的函数即映射关系,是对运算过程的抽象,是用来描述数据之间的映射
// 非函数式
let a = 1, b = 2, c = a + b;
console.log(c)
// 函数式
const aa = (a, b) => {
return a + b
}
let c = aa(1, 2)
console.log(c)
函数式编程语言的特性
函数是一等公民
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
高阶函数
什么是高阶函数?
- 函数可以作为参数传递给另一个函数
- 函数可以作为另一个函数的返回结果
函数作为参数
// 模拟forEach,打印数组中的每一项
const forEach = (arr, fn) => {
for (let i = 0; i < arr.length; i++) {
fn(arr[i])
}
}
let c = [1, 2, 3, 4, 5];
forEach(arr, (item) => {
console.log(item)
})
// 模拟filter,把满足条件的每一项存储下来并返回
const filter = (arr, fn) => {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
result.push(arr[i])
}
}
return result
}
// 测试
let arr = [1, 2, 4, 7, 8];
filter(arr, (item) => {
return item % 2 === 0
})
函数作为返回值
const makeFn = () => {
let msg = 'hello function';
return () => {
console.log(msg)
}
}
makeFn()();
// 模拟lodash中的once,只执行一次
const once = (fn) => {
let done = false;
return function () {
if (!done) {
done = true;
return fn.apply(this, arguments);
}
}
}
let pay = once((money) => {
console.log('gg', money)
console.log(`支付了${money}RMB`)
});
pay(9);
使用高阶函数的意义
高阶函数是用来抽象通用问题,抽象可以帮我们屏蔽细节,我们只用关注实现的目标
// 模拟常用的高阶函数:map、every、some
// map 遍历数组中的每一项,将满足条件的项存入新的数组并返回
const map = (array, fn) => {
let result = [];
for (let value of array) {
result.push(fn(value))
}
return result
}
// 测试
console.log(map([1, 2, 3, 4], v => v * v))
// every 检测数组所有元素是否都符合指定条件,有一项不满足条件就返回false,剩余的元素不会再进行检测。
const every = (array, fn) => {
let result = true;
for (let value of array) {
if (!fn(value)) {
result = false;
break;
}
}
return result
}
// 测试
console.log(every([5, 7, 6,], v => v > 10));
// some 检测数组中的元素是否满足指定条件,如果有一个元素满足指定条件就返回true,剩余的元素不会再继续检测。
const some = (array, fn) => {
let result = true;
for (let value of array) {
if (fn(value)) {
result = true;
}
}
return result
}
// 测试
console.log(some([5, 8, 9, 3], (v) => v > 2));
闭包
含义:
函数和其周围的状态(语法环境)的引用捆绑在一起形成闭包。
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕会从执行栈上移除,但是堆上的作用于成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
function makePower(power) {
return function (number) {
return Math.pow(number, power)
}
}
// 求平方
let power2 = makePower(2);
// 求立方
let power3 = makePower(3);
console.log(power2(2))
console.log(power2(3))
console.log(power3(4))
纯函数
纯函数的概念
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
纯函数就类似数学中的函数,用来描述输入和输出的关系
lodash是一个纯函数的功能库,提供了对数组,数字,对象,字符串,函数等操作方法
-
数组的slice和splic分别是纯函数和不纯的函数
slice返回数组中的指定部分,不会改变原数组
splice对数组进行操作返回该数组,会改变原数组
let numbers = [1, 2, 3, 4, 5]
//纯函数
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
numbers.slice(0, 3) // => [1, 2, 3]
// 不纯的函数
numbers.splice(0, 3) // => [1, 2, 3]
numbers.splice(0, 3) // => [4, 5]
numbers.splice(0, 3) // => []
纯函数代表:lodash
纯函数的好处:
1、可缓存,因为纯函对于相同输入始终具有相同输出,所以可以把纯函数的结果缓存起来
2、可测试,纯函数让测试更方便
3、并行处理,在多线程环境下并行操作共享的内存数据很有可能出现意外的情况;纯函数只依赖参数,不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(web worker)
函数的副作用:
// 不纯的
let mini = 18
function checkAge(age) {
return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge(age) {
let mini = 18
return age >= mini
}
副作用让一个纯函数变的不纯,如上例,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
副作用的来源:配置文件、数据库、获取用户的输入...
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和重用性,同时副作用会给程序带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
柯里化
当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果
lodash中的柯里化
// 柯里化案例
''.match(/\s+/g);// 提取字符串中的空白字符
''.match(/\d+/g);// 提取字符串中的数字
const _ = require('lodash');
const match = _.curry((reg, str) => str.match(reg));
const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
// console.log(haveSpace('hello word'))
// console.log(haveNumber('333adg'))
// 操作数组
const filter = _.curry((func, array) => array.filter(func))
// 获取数组中具有空白字符的元素
const findSpace = filter(haveSpace);
console.log(findSpace(['fsf,vvv ,kkk l']));
柯里化实现原理
// 模拟柯里化实现原理
const getSum = (a, b, c) => a + b + c;
// 模拟lodash中curry方法
const curry = (func) => {
return function curriedFn(...args) {
// 判断形参和实参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
const curried = curry(getSum);
console.log(curried(1)(2, 3)); // 6
console.log(curried(1, 2)(3));// 6
console.log(curried(1, 2, 3));// 6
总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住啦某些固定参数的新函数
这是一种对函数参数的缓存
让函数变得更灵活,让函数的粒度更小
可以把多元函数转成一元函数,可以组合使用函数产生强大的功能
函数组合
如果一个函数需要经过多个函数处理才能得到最终只,这个时候可以把中间过程的函数组合成一个函数
// 函数组合
const compose = (f, g) => {
return function (value) {
return f(g(value))
}
}
// 反转数组
const reverse = (array) => {
return array.reverse()
}
// 获取数组的第一个元素
const first = (array) => {
return array[0]
}
const last = compose(first, reverse);
console.log(last([1, 2, 3, 4, 5, 6,]));
lodash中的组合函数
// 模拟lodash中的flowRight
// const _ = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
// const compose = (...args) => {
// return (value) => {
// return args.reverse().reduce((acc, fn) => {
// return fn(acc)
// }, value)
// }
// }
// ES6
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse);
console.log(f(['one', 'two', 'three']));
函数结合律
// 函数结合需要满足结合律
const _ = require('lodash');
const f = _.flowRight(_.toUpper, flowRight(_.first, _.reverse));
console.log(f(['one', 'two', 'three']));
函数组合调试
// 函数结合 调试
// NEVER SAY DIE --->never-say-die
const _ = require('lodash');
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn));
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower),trace('split之后'), split(' '));
console.log(f('NEVER SAY DIE'))
lodash模块数据优先,函数置后
lodash/fp模块函数优先,数据置后
// lodash和lodash/fp模块中map方法的区别
const _ = require('lodash');
console.log(_map(["23", "8", "10"]), parseInt);
// parseInt("23",0,array)
// parseInt("8",1,array)
// parseInt("10",2,array)
const fp = require('lodash/fp');
console.log(fp.map(parseInt, ["23", "8", "10"]))
PointFree
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参
数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
1、不需要指明处理的数据
2、只需要合并运算过程
3、需要定义一些辅助的基本运算函数
const f = fp.flowRight(fp.join('-'), trace('map之后'), fp.map(fp.toLower), trace('split之后'), fp.split(' '));
案例
// 非pointFree模式
// const f = (word) => word.toLowerCase().replace(/\s+/g, '_');
// pointFree模式
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, "_"), fp.toLower);
console.log(f('hello word'))
//把一个字符串的首字符提取并转换成大写,使用. 作为分隔符
// word wild web ==>W. W. W
// pointFree模式
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '))
console.log(firstLetterToUpper('word wild web'))
函子
函子的概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
函子首先是一个容器,它包含了值和值的变形关系,这个变形关系就是函数。
函子可以把函数式编程副作用控制在可控的范围内,包括处理异常,异步操作等。
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
函子的基本构造
函子就是一个特殊的容器,它可以由对象来实现,这个对象中包含了值,这个值永远不会对外公布,有一个map方法,用来操作这个值。还有一个of方法,用来生成一个新的容器。
// Functor
class Container {
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.of((fn(this._value)))
}
}
let r = Container.of(5).map(x => x + 1).map(x => x * x);
console.log('r', r);
这里总结一下函子的使用
- 程序运算不会直接操作值,而是通过函子来完成
- 由map处理后返回的是一个新的对象,我们可以继续链式的操作值
- 我们可以把函子想象成一个盒子,盒子中封装着一个值,当我恩处理盒子中的值的时候我们要用到盒子专门改变值的工具:map,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理,最终map方法返回一个包含新值的盒子(函子)。
MayBe函子
函子会接收各种函数来处理内部的值,这里就有可能遇到错误,我们需要对这些错误做处理,MayBe函子的作用就是对外部的空值情况做处理。
MayBe函子的构造就是在map中设置空值检查
class Maybe{
static of(value){
return new Maybe(value)
}
constructor(val){
this._value = val
}
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
虽然 MayBe函子可以避免出现错误,但是多次调用map时我们并不知道哪里出现了错误
Either函子
Either函子与if...else处理很相似。它内部有两个值,左值和右值。右值通常代表正常的值,左值是当右值不存在或错误时的默认值
class Either {
static of(left,right){
return new Either (left,right))
}
constructor(left,right){
this.left = left
this.right = right
}
map(fn){
return this.right ? Either.of(this.left,fn(this.right)) : Either.of(fn(this.left),right)
}
}
此外,Either函子另一个用途是替代try...catch,使用左值来表示错误
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
ap函子
函子中的值有可能是数值,也有可能是一个函数,我们想让值为函数的函子用另一个函子中的值运算,我们就可以用ap函子
function add(x) {
return x + 1
}
const A = Functor.of(2)
const B = Functor.of(add)
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val))
}
}
//我们想让B函子的值使用A函子的值
Ap.of(add).ap(Functor.of(2))
凡是部署了ap方法的函子,就是ap函子。ap函子的意义在于对多参数的函数,可以从多个容器中取值,实现函子的链式调用。
Monad 函子
函子中的值可以接受任何值,所以函子之中可以包含另一个函子。这样就会造成函子多层嵌套的问题。取值时会很不方便。Monad函子的作用就是:总是返回一个单层的函子,它有一个FlatMap方法,与map方法作用相同,唯一的区别就是如果生成了一个嵌套函子,它会取出后者的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {//f是一个函子
return this.map(f).join();
}
}
如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平。
IO函子
I/O是一个不纯的操作,普通的函数式编程无法处理,所以使用IO函子操作
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO (function () {
return value
})
}
constructor(fn) {
this._value = fn
}
map (fn){
return new IO (fp.flowRight(fn,this._value));
}
}
- IO函子中的_value是一个一个函数,这里是把函数作为值来处理
- IO函子可以把不纯的动作存储到_value中,延迟这个不纯的操作(惰性执行),包装当前的操作是纯的操作
- 把不纯的操作交给调用者来处理