函数式编程
先举个栗子
需求:转换数组为JSON数组
// 数据源
['牛','马','牛马','健康马']
//转换
[{name:'小牛'},{name:'小马'},{name:'小牛马'},{name:'小健康马'}]
我们来采用命令式
const arr = ['牛', '马', '牛马', '健康马'];
const result = [];
for (let i = 0; i < arr.length; i++) {
let name = arr[i];
let changeName = `小${name}`;
result.push({ name: changeName });
};
console.log(result);
一般面向过程都是这样,以此执行下面的方法
- 第一步遍历数组
- 定义存储结果变量result
- 抽出数组每一项
- 定义改变后结果拼接
- push结果进result
- 返回结果
需求解决了吗?当然解决了!但是这样做存在什么问题?中间变量很多,变量多了容易搞不清楚,得从头到尾读一遍,才能了解处理的过程,出问题就很难定位。
函数式写法
- 把 String 数组转换成 Object 数组
convertNames :: [String] ——> [Object]
- String 数组转换成 Object 数组 可以拿出来做 String转换Object
covertObj :: String ——> Object
- String 转换 Object 中间过程需要对 String 进行处理
- changeName():String ——> String
- genObj():任意类型 ——> Object
它的着眼点是函数,而不是过程,强调通过 函数组合变换 解决问题。
概念
-
高中数学书里就早就有了函数的概念: 描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输入值
函数实际上算是一种映射关系,而且可以组合,我们可以函数套函数,让一个函数的输出类型编程另一个函数的输入类型:f(g(x)),数学上一般是这样的写法。 -
编程工作就是找映射关系,函数式编程就相当于是流水线工作。
特点
-
函数是一等公民
这是函数式编程的大前提,函数的地位和其他数据类型一样,处于同等地位,给以赋值给其他变量,也可以作为另一函数的参数,或者别的函数的返回值。const funA = funB(gen('aaa'), name);
-
声明式编程
函数式编程很多时候都在声明我需要做什么,而非怎么做。代码可读性高,我们不需要去考虑具体怎么实现。 -
惰性执行
函数只在需要的时候才去执行,不产生中间变量,从头到尾都在写函数,只有最后调用产生结果。 -
无状态和数据不可变(核心概念)
- 无状态: 一个函数,无论何时运行,都像第一次运行一样,给相同的输入,产生相同的输出,不依赖外部状态
- 数据不可变: 所有数据不可变,如果你想修改一个对象,应该创建新对象来修改,而不是修改已有对象
这种函数一般是纯函数
纯函数
-
不依赖外部变化(无状态)
-
没有副作用(数据不变)
举个栗子:
非纯函数const testObj = { name: '测试对象' } //全局引用了testObj,非参数传递 const saySomething = (str) => { return `${testObj.name}:${str}` } // 通过引用类型修改了入参 const changeName = (obj, name) => { obj.name = name } changeName(testObj,'新名称') //{ name: '新名称' } saySomething('厚礼谢!') //新名称:厚礼谢!
纯函数
const testObj = { name: '测试对象' } const saySomething = (obj, str) => { return `${obj.name}:${str}` } const changeName = (obj, name) => { return { ...obj, name:name } } changeName(testObj, '新名称') //{ name: '新名称' } saySomething(testObj, '厚礼谢!') //测试对象:厚礼谢!
-
方便测试优化:一个函数永远返回相同结果,发现问题时,可以很容易判断函数的返回结果,优化函数内部不会影响其他代码执行。
-
可缓存:可以提前缓存函数的执行结果。
-
自动文档化:没有副作用,每个函数的功能固定,更容易礼节
-
减少bug,减少共享状态
函数式编程构建流水线(柯里化和函数组合)
-
加工站 —— 柯里化
多元函数转依次调用的单元f(a,b,c) ——> f(a)(b)(c)
函数的返回值有且只有一个,如果我们想要顺利组装,那上一个输入结果刚好流向下一个输入。流水线上的加工站必须是单元函数。
柯里化的处理结果就是单输入的 -
流水线 —— 函数组合
将多个函数组合成一个函数const compose = (f, g) => { return (x) => { return f(g(x)); } } const f = (x) => { return x + 1; } const g = (x) => { return x * 2; } const fg = compose(f, g) fg(1) //3 x * 2 + 1
形成了一个全新的函数fg,并且这种结合方式可以满足结合律(牛逼啊!)
compose(f,compose(g,t)) === compose(compose(f,g),t) === f(g(t(x)))
只要保证f,g,t顺序一致,返回的结果都是f(g(t(x)))
来个复杂的?const compose = (...fns) => { return (...args) => { return fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); } } const f = x => x + 1 const g = x => x * 2 const t = (x, y) => x + y let fgt = compose(f, g, t) console.log(fgt(1, 2)) //7 3—>6——>7
应用
假设我们要大写数组最后一项的字符串 反转,取首,大写,打印
命令式写法log(toUpperCase(head(reverse(arr))))
面向对象的写法
arr.reverse() .head() .toUpperCase() .log()
链式调用从书面上看上去很容易,但是链式调用的函数数量是有限的,需求是无限的
函数组合的方式const upperLastItem = compose(log,toUpperCase,head,reverse)
组合函数会经过reverse —> head ——> toUpperCase ——> log 这些函数都是纯函数,你可以拿去组合使用,不用考虑任何顾虑,这种方式类似于pipe(管道)不过我们是从右往左的方式执行的,Lodash Ramda库里的pipe方法从左往右组合
实际操作
/*
* 编写一个过滤用户信息的函数
* 统计18岁以下
* 统计男性
* 统计18岁以下男性
* 且记录他们的name和age
* 更新一个指定名称用户的年龄
*/
const testData = [
{
sex: "M",
name: "3U5",
age: 17,
grade: 82
},{
sex: "F",
name: "Hu8M",
age: 17,
grade: 62
},{
sex: "M",
name: "8GPC",
age: 18,
grade: 79
},{
sex: "F",
name: "QC3",
age: 17,
grade: 59
}
];
const pipe = function () {
const args = [].slice.apply(arguments);
return function (x) {
return args.reduce((res, cb) => cb(res), x);
}
}
const curry = (fn) => {
return function recursive(...args) {
// 如果args.length >= fn.length则表明传入了足够的参数,此时调用fn并返回
if (args.length >= fn.length) {
return fn(...args);
}
// 否则表明没有传入足够的参数,此时返回一个函数,用这个函数接受后面传递的新参数
return (...newArgs) => {
// 递归调用recursive函数,并返回
return recursive(...args.concat(newArgs));
};
};
};
//判断key值小于某val 返回Boolean
const propGt = (key, val, object) => object[key] < val
//判断key值相等某val 返回Boolean
const propEq = (key, val, object) => object[key] === val
//获取对象key值构建新对象
const pickAll = (keys, object) => {
const res = {};
keys.map(key => res[key] = object[key])
return res
}
//柯里化转换成单元函数
const curryGt = curry(propGt)
// 过滤某一年龄下
const filterAge = curryGt('age', 18) //filterAge(object)
const ageUnder18 = (data) => data.filter(filterAge)
//过滤某一性别
const curryEq = curry(propEq)
const filterSex = curryEq('sex', 'F') // filterM(object)
const sexM = (data) => data.filter(filterSex)
/**既过滤性别也过滤年龄 */
const getAgeUnder18Male = pipe(ageUnder18, sexM)
// console.log(getAgeUnder18Male(testData))
//获取name和age
const cPickAll = curry(pickAll)
const pickKeys = cPickAll(['name', 'age', 'sex'])
const pickData = (data) => data.map(pickKeys)
// console.log(pickData(testData))
//获取18岁以下男性的name和age
const pickMaleUnder18ByNameAndAge = pipe(ageUnder18, sexM, pickData)
console.log(pickMaleUnder18ByNameAndAge(testData))
/**通过指定key对应的对象改变val值 */
const upDatePropBy = curry((key, changeVal, keyVal, newVal, object) => {
if (object[key] === keyVal) {
let newObj = { ...object }
newObj[changeVal] = newVal
return newObj
} else {
return { ...object }
}
})
const upDateByName = upDatePropBy('name') //upDatePropBy(changeVal, keyVal, newVal, object)
const upDataAgeByName = upDateByName('age') // upDataAgeByName(keyVal,newVal,object)
const updateUsersAgeByName = (name, val, data) => {
return data.map(upDataAgeByName(name, val))
}
// console.log(updateUsersAgeByName('Uktg', 300, data))
优缺点
1. 优点
- 开发快速,代码简洁:代码都是函数拼接,可复用率告,减少代码重复
- 大量声明式,便于理解
- 没有副作用
- 更少的出错:每个函数很小,相同的输入永远得到相同的输出
2. 缺点
- 性能问题:包装过度
- 资源占用:遵循状态不可变,需要拷贝创建新对象,会给垃圾回收带来比较大的压力
- 在函数式编程中,为了实现迭代,通常会采用递归操作,为了减少递归的性能开销,我们往往会把递归写成尾递归形式,以便让解析器进行优化。JS 是不支持尾递归优化的