什么是函数式编程

191 阅读6分钟

一、函数式编程

函数式编程(functional Programmiing , FP), FP 是编程方式之一, 例如: 面向对象编程也是其中之一。

  1. 面向对象编程思维方式:把现实世界的一些事物进行归类,抽象成程序中的类和对象。 通过封装、继承和多态来演示事物与事物之间的联系。
  2. 函数式的思维方式: 我理解的就是相当于数学中的公式,只是前人根据经验和总结:出来了一套公式: y = x + b; 我们只需要代入就能求出我们所需要的结果。不管世界再怎么变需求怎么变。公式是万变不离其宗的。不会受外界因素影响。输入同样的值, 输出是不变的。
函数是一等公民
  1. 函数可以储存在变量中
  2. 函数作为参数
  3. 函数作为返回值 在js中函数函数虽然是一个普通的对象, 但是它既能做参数 还可以赋值给变量(只是将地址储存在变量中),还可以作为返回值。 所以这就多了很多可能性, 也就有了千变万化的写法。 实例:
const arr = [1,233,45];
Array.prototype.newforEach = function (fn) {
  for (let i in this) {
    if (this.hasOwnProperty(i)) {
      fn&&fn(this[i], i*1, this);
    }
  }
}
arr.newforEach(function (i, item, val){
  console.log(i, item, val);
});

Array.prototype.newfilter = function (fn) {
  let arr = [];
  for (let i in this) {
    if(this.hasOwnProperty(i)) {
      if (fn(this[i])) {
        arr.push(this[i]);
      }
    }
  }
  return arr;
};

Array.prototype.newmap = function (fn) {
  const arr = [];
  for (let i in this) {
    if (this.hasOwnProperty(i)) {
      arr.push(fn(this[i], i, this));
    }
  }
  return arr;
}
const res = arr.newmap(function (i, index, val) {
  console.log(i, index, val, 9999)
  return i*100;
});
  // 1 0 [ 1, 233, 45]
  // 233 1 [ 1, 233, 45 ]
  // 45 2 [ 1, 233, 45 ]
高阶函数
  1. 抽象可以帮我们屏蔽细节,只需要关注我们的目标。
  2. 高阶函数是用来抽象通用问题的。 个人理解: 高阶函数就是类似与黑盒子一样,我们只需要关注套公式直接用就行并不需要关注里面的实现细节和原理。
常用的高阶函数。

newevery: 数组需要都满足条件则返回true、 否则返回false; \color{red}{if (!result) {}} newsome: 数组有任意个元素满足则返回true, 否则返回false。 \color{red}{if (result) {}}

Array.prototype.newevery = function(fn) {
  let result = true;
  const data = this;
  for (let i in data) {
    if (this.hasOwnProperty(i)) {
      result = fn(this[i], i, this);
      if (! result ) {
        break;
      }
    }
  }
  return result;
}
console.log(arr.newevery(function (i) {
  return i > 1;
}))
闭包的概念。

闭包的本质: 函数在执行的时候会释放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放。因此内部函数依然可以访问外部函数的成员。

代码解释:

const debounce_1 = (fn, speed) => {
  const timer = speed;
  const clearTimer = null; 
  return function () {
    clearTimeout(clearTimer); // 因为闭包如果上一个setTimeout正在执行会被销毁掉。
    const centext = this;
    const arges = arguments;
    clearTimer = setTimeout(() => {
      fn&&fn.apply(centext, arges);
    }, timer)
  } 
}

function fn (money) {
    return function (achievements) {
    return money + achievements;
    };
  }
  const fn1 = fn(13000);
  const fn2 = fn(18000);

  console.log(fn1(3000));
  console.log(fn2(3000));

纯函数的好处

纯函数的可缓存: 方便并行处理。可测试。

function memoize (fn) {
      let cache = {};
      return function () {
        let key = JSON.stringify(arguments);
        cache[key] = cache[key] || fn.apply(fn, arguments);
        console.log(cache)
        return cache[key];
      }
    }
    function getArea (r) {
      console.log(r)
      return Math.PI * r * r;
    }
    const getAreaWithMeory = memoize(getArea);
    console.log(getAreaWithMeory(1));
    console.log(getAreaWithMeory(2));
    console.log(getAreaWithMeory(3));
    console.log(getAreaWithMeory(4));

副作用

  1. 其实副作用就是依赖于外部环境的函数。按照定义来说他就不是纯函数。副作用是不可能完全禁止的。我们需要控制它在可控范围。

模拟curry 和 柯里化样例。

/**
 * 模拟curry
 * 需求有一些函数有多个参数: 如果每个参数都传的话
 * 它就是一个纯函数。但是因为某些参数是需要依赖环境
 * 来作出变化的。这个时候他就不是纯函数。 
 * 这个时候就需要我们根据环境作出改变。
 * 思路: 
 * 1. 根据函数的length判断形参的长度。
 * 2. 根据剩余参数获取实际参数。
 * 3. 如果形参等于实际参数则表示正常请求目标函数。
 * 4. 如果实际参数小于形参:我们需要返回一个参数来接收其他的参数。
 * 5. 这个时候我们递归调用实际调用的那个函数。同时需要将上次的参数合并传给这个参数
 * 6. 直到形参等于实际参数。才会跳过判断调用目标函数。
 * */ 
function simulation (fn) {
  return function simulationEnd (...args) {
    if (fn.length > args.length) {
      return function () {
          return simulationEnd(...args.concat(Array.from(arguments)));
      }
    }
    return fn(...args);
  }
}
function makePower (count, nums) {
  return Math.pow(count, nums);
};
const countOne = simulation(makePower);
const countTwo = countOne(12);
console.log(countTwo(1), makePower.length);
// 1728 2

总结:

柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数。 这是一种对含税参数的缓存。 让函数变得更灵活,让函数的颗粒度更小哦。 可以把多元函数转换一元函数,可以组合使用函数产生强大的功能。

函数的组合概念

当我们写了特别多的纯函数,这个时候我们需要将他们组合在一起。这个时候就会形成像洋葱一样包含着在一起。如果出现了某一个函数出现问题,就不容易查找。而且代码的可读性就会比较差。有个概念叫做扁平化处理。

函数的组合 和 组合函数模拟

/**
* 
* 我们需要实现的功能是什么一样的呢?
* 简单描述下:
* 1. 我写了N个纯函数对数组做一系列的操作
* 2. 我需要将所有需要运行的纯函数交给一个函数
* 让他去运行并且并且返回一个新的函数
* 3. 当我运行这个新的函数的时候只需要将我需要操
* 作的目标传进去便可以返回出我想要的结果。
*/
const arr = [1,2,3,9]
const fn1 = arr => {
return arr[0]
};
const fn2 = arr => arr.reverse();

// console.log(fn1(fn2(arr)), 88888); // 这就是类似洋葱的代码可读性差。

// 接下来我们优化下。
function combination (f1, f2) {
return function () {
  return f1(f2(Array.from(arguments)));
}
}
const group = combination(fn1, fn2);

// console.log(group(...arr)) // 这个时候我们输出的值是不是跟上面是一样的。
// 其实我们封装这个组装函数是有问题的,跟第一个方法一样没什么区别,不如直接调用。

// function splicing (...agrs) {
//   return function (val) {
//     return agrs.reverse().reduce(function(acc, fn) {
//       return fn(acc);
//     }, val)
//   }
// }
// const newGroup = splicing(fn1, fn2);
// console.log(newGroup(arr), 999999)

const optiSplicing = (...agrs) => val => agrs.reverse().reduce((acc, fn) => fn(acc), val); 
const newOptiSplicing = optiSplicing(fn1, fn2);
console.log(newOptiSplicing(arr), 999999)

函数的组合需要满足结合律

任意两个函数组合起来且跟原先的执行顺序一样的情况下。结果都一样。

const optiSplicing = (...agrs) => val => agrs.reverse().reduce((acc, fn) => fn(acc), val); 
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const Font = arr => arr.toUpperCase();
// const newFn = optiSplicing(optiSplicing(Font, first), reverse);
const newFn = optiSplicing(Font, optiSplicing(first, reverse));
console.log(newFn(['wqw', 'asa']), 8888)
console.log(Font(first(reverse(['wqw', 'asa']))), 999)

函数组合——调试

const _ = require('lodash');
// const log = v => {
//   console.log(v);
//   return v;
// }
const trace = _.curry((tag, v) => {
  console.log(tag, v);
  return v;
})
// _.split(str, sep);
const split = _.curry((sep, str) => _.split(str, sep) );
// _.toLower()
// join
const join = _.curry((sep, str) => _.join(str, sep) );
const map = _.curry((fn, arr) => _.map(arr, fn))
const f = _.flowRight(join('-'), trace('map  之后'), map(_.toLower), trace('split 之后'), split(' '));

console.log(f("NEVER SAY DIE"))

总结:

我们需要将所有处理数组的函数进行柯里化。也就是我们传入一个或者小于形参都会返回一个新的函数。这里我传入的参数就是我们需要根据环境特别处理的参数。 然后我们需要将众多柯里化的函数需要进行执行。我们需要用到函数的组合。同时进行调试,和调试函数。