前端百日进阶-1.函数式编程

127 阅读5分钟

我记得我最开始接触到函数式编程是在使用reduxcompose函数,当时我看了compose的源码还非常的懵逼。知道后来知道了函数式编程才明白其中奥义。

如果大家有用过redux应该都会很熟悉下面的代码。

let store = createStore(
  counter,
  compose(applyMiddleware(logger))
);

有些时候在实际开发中只是创建store其实并不满足我们的开发需求这时候compose就可以用来对store进行增强比如添加一个打印日志之类的日志。

官方源码如下:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

如果大家不了解函数式编程可能就不太清楚上面这个函数的意义, 如果知道了函数式编程在阅读起这些源码应该是非常好理解的。在React以及Vue 3也都使用了函数式编程这种编程范式。所以大家学习函数式编程还是非常有必要的。

什么是函数式编程

函数式编程是一种编程范式,和我们平时最常使用的命令式编程相比较,函数式编程主要是关注于数据的映射。 这里要提的一点是函数式编程中的函数并不是指程序中的函数,而是数学关系中的映射关系.且每一次相同的输入都要得到相同的输出(纯函数)。

函数式编程中的概念

js中有很多函数概念,如果了解了的话会更利于理解函数式编程。

函数是一等公民

在编程语言中一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。而在JavaScript中函数也是一等公民。

高阶函数

当函数作为参数传递时或者函数被当作返回值时都被称为高阶函数。

纯函数

用于描述输入和输出之间的关系。同样的输入一定会返回同样的输出。

函数副作用

当函数依赖外部变量会产生函数副作用让函数变的不纯。外部扩展都会产生副作用。

const age = 18
// 这个函数的返回值依赖外部变量age
function checkAge() {
	return age > 18
}

副作用会使一个函数无法预测所以应该尽量避免副作用的产生。

闭包

概念: 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

image-20210417212858035.png

Snipaste_2021-04-17_21-36-50.png

个人理解:

当在外部函数(test)的内部调用了内部函数(cc),并且内部函数(cc)引用了外部函数(test)的变量,此时会产生闭包。

闭包用于存储外部函数(test)变量,当内部函数(cc)被返回出去并且被调用时,由于闭包引用了外部函数(test)变量,所以这个变量(a)依然不会被销毁。

Snipaste_2021-04-17_21-37-52.png

未被引用不产生闭包。

函数柯里化

函数柯里化(currying)又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

function getAge(env, age) {
	if (env === 'dev') {
		return age > 18
	}
	
	if (env === 'prod') {
		return age > 25
	}
	
	return age > 30
}

// 每次调用都会去传入dev
console.log(getAge('dev', 30))

假设我想一直使用dev的但又不想多次传入dev就可以把函数柯里化后执行

// 用于将函数柯里化
function curry(func) {
    return function curryFn(...args) {
        if (args.length >= func.length) {
            return func(...args)
        }

        return function(...args2) {
            return curryFn(...args.concat(args2))
        }
    }
}

const getAge = curry(function (env, age) {
	if (env === 'dev') {
		return age > 18
	}
	
	if (env === 'prod') {
		return age > 25
	}
	
	return age > 30
})

// 这样做可以避免每次调用多传入dev
const getDevAge = getAge('dev')
console.log(getDevAge(30))
// 也可以同时传入多个
console.log(getAge('prod',30))

函数组合

如果仅仅使用纯函数和柯里化非常容易写出洋葱代码:a(b(c()))这样不容易读懂所以需要用到函数组合.

函数组合可以把小颗粒度的函数组合在一起返回一个新的函数,上面提到的compose就是使用的函数组合

function compose(...funcs) {
    return function(value) {
        return funcs.reduce((prev, fn) => fn(prev), value)
    }
}

function toUpper(str) {
    return str.toUpperCase()
}

function first(str) {
    return str[0]
}

// 从右往左执行,右边函数的返回值为左边函数的参数
console.log(compose(toUpper, first)('hello'))

函子

作用:函子主要用于把副作用控制在可控范围之类

可以把函子理解为一个特殊的容器内部包含有一个value且可以通过map来对这个value进行操作

class Container {
    static of (value) {
        return new Container(value)
    }

    constructor(value) {
        this.value = value
    }

    map(func) {
        return Container.of(func(this.value))
    }
}

Container.of(1)
    .map((v) => {
        console.log(v)
        return v + 1
    })
    .map((v) => {
        console.log(v)
    })

函子存在的主要作用就是抽离副作用,让函子去处理异常和IO

Maybe函子

maybe函子在函数处理过程中返回了null会避免null去调用方法而照成的错误

class Maybe {
    static of(value) {
        return new Maybe(value)
    }

    constructor(value) {
        this.value = value
    }

    isNothing() {
        return this.value === null || this.value === undefined
    }

    map(func) {
        return this.isNothing() ? Maybe.of(null) : Maybe.of(func(this.value))
    }
}

const t = Maybe.of(1).map((v) => {
    return null
}).map((v) => {
    // 如果使用Container会在这里报错
    v.split('')
})

console.log(t)

IO函子

IO函数=子可以把不纯的操作抽离到执行时传入

let _ = require('lodash');

class IO {
    static of(value) {
        return new IO(function() {
            return value
        })
    }

    constructor(value) {
        this.value = value
    }

    map(func) {
        return new IO(_.flowRight(func, this.value))
    }
}

const t = IO.of(process).map(p => p.execPath)

console.log(t.value())

今日学习总结

  1. 学习了函数式编程概念
  2. 高阶函数,闭包,函数是一等公民的概念
  3. 函数柯里化,纯函数,函数组合灯函数式编程基础
  4. 函子

在学习过程中有写到组合和柯里化的一些原理,但是在实际使用中其实并不需要自己去写。比如说使用现成的函数式编程库:lodash,folktale之类的

let _ = require('lodash');

// 函数柯里化
let abc = (a, b, c) => a + b + c;
let curried = _.curry(abc);
let result = curried(1)(2)(3);
console.log(result);  
const { toUpper, first } = require('lodash');
let _ = require('lodash');

// 函数组合
const getFirstUpper = _.flowRight(toUpper, first)
console.log(getFirstUpper('hello'))

如果想要更加系统和详细的学习函数式编程的使用可以参阅书籍:JavaScript函数式编程指南