函数式编程入门级总结

505 阅读5分钟

写在前言

最近学了不少东西,让我耳目一新就是这个函数式编程,虽然以前我都是用面向对象编程方式,这次一接触函数式编程就感觉思路开拓不少,很多人都说函数式是未来的方向,其实我也只是赞同一部分,一个是的确写法比较优雅,解耦,但是呢用了不少闭包,可能存在性能问题,但是总而言之,还是值得一学,毕竟像react,readux就用了大量这种编程风格写法

函数式编程好处

  1. 函数式编程抛弃 this
  2. 更加方便 tree shaking

让开发者更加专注于业务代码的开发

类型

  1. 函数作为返回值
  2. 函数作为参数
//比如
arr.reduce((item)=>fn)

函数作为返回值,也就是闭包

const onece = (fn) => {
  const times = false;
  return () => {
    if (!times) {
      times = true;
      return fn.apply(this, arguments);
    }
  };
};

如果比如有一个需求,要计算不同级别的员工的工资,只需要把这种工资类型赋值给一个函数,然后调用这个函数就可以进行其他的计算

function makeSalary(baseSalary) {
  return (money) => {
    return baseSalary + money;
  };
}

let level1 = makeSalary(12000)
let level2 = makeSalary(15000)

console.log(level1(3000));
console.log(level2(5000))

纯函数

另外纯函数,对相同的输入始终有相同的输出,而且没有任何可观察的副作用 比如slice,但是像splice就是不纯的函数,就是说对原数据进行修改就不属于纯函数 相对于函数式编程而言,必须是纯函数

纯函数的好处:

  1. 可缓存
  2. 可测试,为了单元测试使用

缓存

这里有一个计算圆面积的方法,我们通过一个cache变量就可以把圆面积计算保存下来,用key值作为唯一标识符,下次才有相同参数进来则使用缓存里面的东西,大大的提高性能

这种使用场景让我想起了斐波那契数列计算优化,也是通过类似的手法进行优化,通过一个cache变量存储每次递归的值

function getArea(r) {
  console.log(Math.PI);
  return Math.PI * r * r;
}

function memery(fn) {
  let cache = {};
  return function () {
    let key = JSON.stringify(arguments);
    return (cache[key] = cache[key] || fn.apply(fn, arguments));
  };
}

let fn = memery(getArea);

console.log(fn(4)); // 会发现只打印一次PI
console.log(fn(4));
console.log(fn(4));
console.log(fn(4));
console.log(fn(4));
console.log(fn(4));

柯里化

柯里化:当一个函数有多个参数的时候,我们可以先传递一部分参数返回一个新函数,然后在用新函数去接收剩余的参数

function checkAge(min){
  return function(age){
    return age>=min
  }
}
let checkAge18 = checkAge(18)
console.log(checkAge18(29));

//通过es6来改造
const checkAge=(min)=>(age)=>age>=min

比如有个需求是过滤字符串中有没有数字或者空格

//面向过程写法
str.match(/\s+/g)
str.match(/\d+/g)

//函数式编程
//这里得使用loadash 
const match = curry((reg,str)=>{
  return str.match(reg)
})

const hasSpace = match(/\s+/g)
const filter = curry((func,arr)=>{
  return arr.filter(func)
})
let findSpace = filter(hasSpace)
console.log(findSpace(['dsaf asfd', '测试有空 格', '测试没空格']));

实现神奇的curry函数

如果大家有印象,应该记得18年还是17年,阿里有一道面试题就是要实现这个curry函数,手写这个函数,实现add(1)(2,3); a(1,2,3);a(1)(2)(3)这种计算方式

其实也很简单,就是通过比较形参和实参数量,如果实参小于形参数量,说明还是没有完全传入全部参数,否则立即进行计算,执行函数

function curry(fn) {
  return function curriedFn(...args) {
    if(args.length<fn.length){
      return function(){
         // 这里比较关键,参数必须按照顺序来
        return curriedFn.apply(curriedFn, [...args, ...arguments]);
      }
    }
    return fn.apply(fn, args);
  };
}

函数组合

但是纯函数和柯里化容易写出洋葱圈代码,比如a(b(c()))

这时候就要用函数组合来解决这个问题

这时候要涉及到一个概念,管道,每一个函数都是一个管道节点,

函数组合默认是从右到左执行的

function compose(...args) {
  return function (value) {
    // 从后往前,然后使用reduce
    return args.reverse().reduce((acc, fn) => {
      // 把value 传进来,然后第一次acc就是value的值,下次acc就是fn(value)的值
      return fn(acc);
    },value);
  };
}

const reverse = (arr) => arr.reverse();
const first = (arr) => arr[0];
const toUpper = (s) => s.toUpperCase();

const f = compose(toUpper, first, reverse);
console.log(f(['one', 'tow']));

//es6简写
const compose =(...args)=>(value)=>args.reverse().reduce((acc,fn)=>fn(acc),value)

结合律:其实就是数学中的结合律,先结合某个括号里的内容

const f = compose(toUpper, compose(first, reverse));

另外,组合函数里面的函数只能接受一个参数,因此如果有多个参数必须得使用curry对函数进行柯里化改造 调试:通过log函数打印出每一次的结果

const _ = require('lodash')
// 进行柯里化是为了先穿一部分参数,另外一部分参数等待管道进来
const split = _.curry((sep, str) => _.split(str, sep));
const join = _.curry((sep,arr)=>_.join(arr,sep))
const map = _.curry((fn,arr)=>_.map(arr,fn))
const log = _.curry((tag,msg)=>{console.log(tag,msg);return msg})
// lodash的_.map模块是数据优先,如果用fp.map则是方法优先,就不用通过curry包装
const f = _.flowRight(join('-'), log, map(_.toLower), log('split 之后'), split(' '));

console.log(f('NEW YORK CITY'))

point free模式

point free模式,不需要指明处理的数据,只需合成运算过程,需要定义一些辅助的基本运算函数, 其实就是函数的组合

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 '));

函数式编程应用

比如给页面上一个id名为test的div添加文字,然后我们把这个方法封装起来,方便以后其他div进行类似操作,比如添加一个dom结构什么的。

我们这里使用函数式编程只关注于对这些操作进行合并即可,具体业务逻辑不用关注,大大解耦


  const curry = (fn) => {
    return function handleCurry(...args) {
      if (args.length < fn.length) {
        // 实参小于形参
        return function () {
          return handleCurry.apply(handleCurry, [...args, ...arguments]);
        };
      }
      return fn.apply(fn, args);
    };
  };

  const compose = (...args) => {
    return function (value) {
      return args.reverse().reduce((acc, fn) => {
        return fn(acc);
      }, value);
    };
  };

  const getDom = (className) => {
    return document.querySelector(className);
  };
  const setColor = curry((color, dom) => {
    dom.style.color = color;
  });
  const setContent = curry((text, dom) => {
    dom.innerHTML = text;
    return dom;
  });
  const log = (msg) => {
    return msg;
  };
  let f = compose(setColor('blue'), setContent('ddd'), log, getDom);
  f('#test');
``

本文使用 mdnice 排版