函数式编程
函数式编程
先举个栗子
需求:转换数组为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转换ObjectcovertObj :: 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 是不支持尾递归优化的

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号