1、简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
一、引例:
输出在网页上输出 “Hello World”。
一般实现
document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>'
另一种实现
function printMessage(elementId, format, message) {
document.querySelector(elementId).innerHTML = '<'+format+'>'+message+'</'+format+'>'
}
printMessage('#msg', 'h1', 'Hello World')
上述仍然不是一段可重用的代码
改变需求:需要多次输出hello World
函数式编程
const printMessage = compose(addToDom('msg', h1, echo))
printMessage('Hello World')
分析:根据上述第二行代码可知,compose最终返回一个函数,并且接收了参数'Hello World'
而compose这个函数接收一个函数callback作为参数。callback函数来具体实现代码需求。
其实h1和echo也都是函数。
可以看到我们是将一个任务拆分成多个最小颗粒的函数,然后通过组合的方式来完成我们的任务,这跟我们组件化的思想很类似,将整个页面拆分成若干个组件,然后拼装起来完成我们的整个页面。在函数式编程里面,组合是一个非常非常非常重要的思想。
函数式编程属于声明式编程范式:这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们。
// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2)
}
array; // [0, 1, 4, 9]
// 声明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))
为什么我们要去掉代码循环呢?循环是一种重要的命令控制结构,但很难重用,并且很难插入其他操作中。而函数式编程旨在尽可能的提高代码的无状态性和不变性。要做到这一点,就要学会使用无副作用的函数--也称纯函数
纯函数:是指不管调用多少次函数,在什么作用域下,都是相同的输入有相同的输出。
对于纯函数有以下性质:
- 仅取决于提供的输入,而不依赖于任何在函数求值或调用间隔时可能变化的隐藏状态和外部状态。(不改变全局变量中定义的属性、变量)
- 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数。
二、实际例子
// 命令式代码
function showStudent(id) {
// 这里假如是同步查询
var student = db.get(id)
if(student !== null) {
// 读取外部的 elementId
document.querySelector(`${elementId}`).innerHTML = `${student.id},${student.name},${student.lastname}`
} else {
throw new Error('not found')
}
}
showStudent('666')
// 函数式代码
// 通过 find 函数找到学生
var find = curry(function(db, id) {
var obj = db.get(id)
if(obj === null) {
throw new Error('not fount')
}
return obj
})
// 将学生对象 format
var csv = (student) => `${student.id},${student.name},${student.lastname}`
// 在屏幕上显示
var append = curry(function(elementId, info) {
document.querySelector(elementId).innerHTML = info
})
var showStudent = compose(append('#student-info'), csv, find(db))
showStudent('666')
三、以组合的方式去抽象代码
函数组合就是一种将已被分解的简单任务组织成复杂的整体过程。
需求:给你一个字符串,将这个字符串转化成大写,然后逆序
function oneLine(str) {
var res = str.toUpperCase().split('').reverse().join('')
return res;
}
如果把最开始的需求代码写成这个样子,以函数式编程的方式来写。
var str = 'function program'
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
var toUpperAndReverse = 组合(stringReverse, stringToUpper)
var res = toUpperAndReverse(str)
需求改变时,[A,A,A]
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
// var toUpperAndReverse = 组合(stringReverse, stringToUpper)
// var res = toUpperAndReverse(str)
function stringToArray(str) {
return str.split('')
}
var toUpperAndArray = 组合(stringReverse, stringToUpper)
toUpperAndArray(str)
可以看到当变更需求的时候,我们没有打破以前封装的代码,只是新增了函数功能,然后把函数进行重新组合。
上述代码中,组合是一个函数,同时参数也是函数,返回值也是函数。
function compose(fn1,fn2){
return function(arg) {
return fn1(fn2(arg))
}
}
三个及三个以上:
function multiFuntionCompose(fn1, fn2, .., fnn) {
return function(arg) {
return fnn(...(fn1(fn2(arg))))
}
}
这种恶心的方式很显然不是我们程序员应该做的,然后我们也可以看到一些规律,无非就是把前一个函数的返回值作为后一个返回值的参数,当直接到最后一个函数的时候,就返回。
function compose(...args) {
return (result) => {
return args.reduceRight((result, fn) => {
return fn(result)
}, result)
}
}
四、point-free
在函数式编程的世界中,有这样一种很流行的编程风格。这种风格被称为 tacit programming,也被称作为 point-free,point 表示的就是形参,意思大概就是没有形参的编程风格。
// 这就是有参的,因为 word 这个形参
var snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');
// 这是 pointfree,没有任何形参
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
一个 pointfree 的函数可能是由众多非 pointfree 的函数组成的,也就是说底层的基础函数大都是有参的,pointfree 体现在用基础函数组合而成的高级函数上,这些高级函数往往可以作为我们的业务函数,通过组合不同的基础函数构成我们的复制的业务逻辑。