18-4 函数式编程

函数式编程:
  • 1.纯函数
  • 2.函数的柯里化
  • 3.函数组合
  • 4.Point Free : 暂无翻译,意思是不要命名转瞬即逝的中间变量
  • 5.声明式和命令式
 
一、纯函数:
纯函数的定义就是:对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
这句话听着还是有点云里雾里,我们就以JS的数组来举例说明:
var arr = [1, 2, 3, 4, 5];
// slice方法:对原数组
console.log("slice后:", arr.slice(0, 3), "原数组:", arr); // [1,2,3] , [1, 2, 3, 4, 5]
console.log("slice后:", arr.slice(0, 3), "原数组:", arr); // [1,2,3] , [1, 2, 3, 4, 5]

var arr = [1, 2, 3, 4, 5];
// splice方法:
console.log("splice后:", arr.splice(0, 3), "原数组:", arr); // [1,2,3] , [4,5]
console.log("splice后:", arr.splice(0, 3), "原数组:", arr); // [4,5] , []
console.log("splice后:", arr.splice(0, 3), "原数组:", arr); // [] , []

// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
// Array.splice是不纯的,它有副作用,对于固定的输入,输出是不固定的

// 结论:数组的方法本身就是函数,如果对原数组产生的修改,他就是不纯的,反之没有对原数组进行改变,就是纯函数。
为什么函数式编程会排斥不纯的函数呢?
再看下面的例子:
/* 不纯的;checkage这个函数不仅取决于入参age,还取决于外部变量min,换句话说这个函数的行为需要外部的系统环境决定。
     对于大型系统来说,这种对于外部状态依赖是造成系统复杂性大大提高的主要原因 */
var min = 18;
var checkage = (age) => age > min;

// 纯的:checkage函数把关键数字18硬编码到函数的内部,扩展性差,耦合性大,我们可以用柯里化解决这个问题
var checkage = (age) => age > 18;
二、柯里化函数
// 以下就是柯里化实现:我们把age固定住,然后把入参18传入,这样既做到了解耦,又不会创建中间变量,变成了纯函数
//  纯函数顾名思义,就是只有函数,没有中间变量,这是我不严谨的总结 ^_^
// 柯里化函数:组织入参
function curryFn(fn, ...args) {
  return (..._args) => {
    return fn.call(null, ...args, ..._args);
  };
}
// 判断方法
function judgeFn(age, min) {
  return age > min;
}

var tempFn = curryFn(judgeFn, 20);
var res = tempFn(18);
console.log(res, "柯里化函数"); // true
柯里化的意义:
事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是对参数的“缓存”,是一种非常高效的编写函数的方法。
三、函数组合
// 看这个函数嵌套: h(g(f(x))); 怎么形成这样的“包菜式”代码呢?以下就是包菜式代码的拆解:函数执行时返回另一个函数,再执行又返回另一函数,一层层的
function h(value) {
  // console.log(value, 'h函数中收到的value');  // 5
  return value;
}
function g(value) {
  // console.log(value, 'g函数中收到的value');  //5
  return value;
}
function f(x) {
  return x;
}
h(g(f(5))); // 输出5
// 虽然这也是函数式的代码,但它依然存在某种意义上不的“不优雅”。为了解决函数嵌套的问题,我们需要用到“函数组合”
// 请看下面的例子(1):

function g(val) {
  // console.log(val, 'g函数中val的值');  // 5
  return val;
}
function f(x) {
  return x;
}

var compose = function (g, f) {
  return function (x) {
    return g(f(x));
  };
};
// ES6写法:
var compose = (g, f) => (x) => g(f(x));
compose(f, g)(5); // 5
// 例子2 :内层f(x)的返回值作为g函数的入参执行g函数中的逻辑并返回
var g = (val) => (val += 100);
var f = (x) => x * x;
var compose = (g, f) => (x) => g(f(x));
compose(g, f)(5); // 125
总结:
我们定义的compose就像双面胶一样,可以把任何两个纯函数粘合在一起。当然你也可以扩展出组合三个函数的“三面胶”,甚至"N面胶"
这种灵活的组合可以让我们像拼积木一样来组合函数式代码:
例:
var lastEle = (arr) => arr[arr.length - 1];
var reverse = (arr) => arr.reverse();

var compose = (f1, f2) => (arr) => f1(f2(arr));
compose(lastEle, reverse)([1, 2, 3, 4, 5]); // 1  ->先执行reverse函数把数组反序,然后把反序后的数组作为入参传给lastEle函数取最后一个元素
四、Point Free : 不要转瞬即逝的中间变量
有了柯里化和函数组合的基础知识,下面介绍一下Point Free这种代码风格
/*
 下面代码分析:
  我们定义了一个变量f用于接收一个函数,并且函数中传入一个入参str,函数内部对str处理后并返回。
  乍一看好像平常我们也都这么写代码。
  但是这个str就是一个中间变量,但这个中间变量除了让代码变长一点以外是毫无意义的
*/
var f = (str) => {
  return str.toUpperCase().split(" ");
};
f("abcd efgh"); //['ABCD', 'EFGH']

// 我们对以上代码进行改造
/* 1. 我们把上面要用到字符串方法split和toUpperCase转换成纯函数
   2. 运用函数组合compose 
*/
var split = (x) => (str) => str.split(x);
var toUpperCase = (str) => str.toUpperCase();
var compose = (f, g) => (x) => f(g(x));

var ff = compose(split(" "), toUpperCase);
ff("abcd efgh"); // ['ABCD', 'EFGH']

/* 综上两种写法:我们发现在最后执行的时候都是f("abcd rhgf") 
   但是第一种就会有一个str的参数传入,第二种通过函数组合,直接就把俩方法传入即可,并没有像第一种带来了中间变量str
*/
五、命令式与声明式代码
/* 命令式代码:
   我们通过编写一条又一条的指令去让计算机执行一些动作,这其中一般都会涉及到许多繁杂的细节。
*/
var arr = [];
for (let i = 0; i < 100; i++) {
  arr.push(i);
}

/* 声明式代码:
   我们通过写表达式的方式来声明我们想干什么,而不是通过一步步的指示。
*/
var ary = [
  { name: "zs", age: "18" },
  { name: "ls", age: "19" },
  { name: "ww", age: "20" },
];
var names = ary.map((x) => x.name);
// 综上:命令式代码是展示的过程,告诉我们怎么做;声明式的代码告诉我们做什么,是结果导向
// 函数式编程的一个明显好处就是这种声明式的代码,对于这种无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
// 相反,不纯的不函数式的代码会产生副作用或者依赖外部系统环境,使用他们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的精力来说是极大的负担。
 
 
 
posted @ 2022-04-18 20:41  猎奇游渔  阅读(59)  评论(0编辑  收藏  举报