函数式编程

577 阅读5分钟

主讲:云隐

函数式编程的出现

发展历程:命令(脚本)式 => 面向对象式 => 函数式编程

问题的出现 - 从一道面试题开始

面试题:上接浏览器原理 - 参数 parser

  1. 数组在 url 找那个展示形式:
    • location.search 获取:
      • ?name[]=progressive$%coding&name[]=objective$%coding&name[]=functional$%coding;(其中 $% 是跟后台约定好的空格表示)
  2. 参数提取拼接成数组:
    • ['progressive$%coding', 'objective$%coding', 'functional$%coding']
  3. 手写方法,转换成数组对象:
    • [{ name: 'Progressive Coding'}, { name: 'Objective Coding'}, { name: 'Functional Coding'}]
const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];
const _objArr = [];

const nameParser = (array, objArr) => {
  array.forEach(item => {
    let names = item.split('$%');
    let newName = [];

    names.forEach(name => {
      let nameItem = name[0].toUpperCase() + name.slice(1); // 首字母转大写
      newName.push(nameItem);
    });

    objArr.push({
      name: newName.join(' ')
    });
  });

  return objArr;
};

const result = nameParser(_array, _objArr);
console.log(result);
/* 
  [
    { name: 'Progressive Coding' },
    { name: 'Objective Coding' },
    { name: 'Functional Coding' }
  ]
*/
  • 这种写法是常规的解题思路 - 面向过程;
  • 问题:
    1. 过程中存在逻辑包裹 - 看完整段代码,才能明白在做啥;
    2. 存在临时变量,并且首尾封闭 - 迭代拓展难度高;

解决方案

  • step1:需求分析
    • 需要把 数组 转换成 数组对象
    • [字符串 变成 对象]['progressive$%coding'] => [{ name: 'Progressive Coding'}]
    • 针对本题:nameParse => [objHelper(对象处理器) :: strng > object]
  • step2:模块功能明确 objHelper
    • objHelper = formatName(格式化) + assembleObj(组装对象)
  • step3:功能拆分
    • objHelper = [(split + capitalize + join)] + assembleObj
  • step4:代码实现
/* 
  函数式编程的基础手写实现
*/
const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];

// 原子操作
const assembleObj = (key, x) => {
  let obj = {};
  obj[key] = x;
  return obj;
};
const capitalize = name => name[0].toUpperCase() + name.slice(1);

// 组装描述
// const formatName = 组装合并(join(' '), map(capitalize), split('$%')); // 组装顺序:从后往前
// const objHelper = 组装合并(assembleObj('name'), formatName);
const formatName = R.compose(join(' '), map(capitalize), split('$%')); // 组装顺序:从后往前
const objHelper = R.compose(assembleObj('name'), formatName);
const nameParser = map(objHelper);

// 使用
nameParser(_array);

/* 
  上面的就是函数式编程的思想,解决了上面的两个问题
*/

**面试题:**正确的遍历 - for、forEach、map、filter、sort...

  • 基本使用方法
  • 能否 return,能否 break
  • 遍历是否存在风险
    • 例如 forEach 遍历必须是数组
  • 返回值是什么
    • for、forEach:无返回值
    • map、filter、sort:返回值是数组

本质作用:

  • for:通用遍历;
  • forEach:遍历逻辑处理;
  • map、filter、sort:生成处理后的数组;
  • filter:生成处理后的数组 - 过滤;
  • sort:生成处理后的数组 - 排序;
  • (forEach、(map、(filter、sort...))):包含的关系;
let _class = 'functional';
let isOvered = false;
let classArr = ['progressive', 'objective', 'functional'];

/* 
  要求:课程列表 classArr,判断列表里是否包含 _class,如果包含 isOvered 改变为 true
  问题:遍历使用 forEach 还是 map?
  答案:使用 forEach
    forEach:重点是遍历逻辑处理,无论是对外部遍历做处理,还是对循环数据 item 做处理;
    map:重点是生成处理后的数组,对内部数据的操作,不是为了遍历;这里是改变了外部变量 isOvered,不适合用 map;
*/

// 使用 forEach 情况 - 改变了外部的变量
classArr.forEach(item => {
  isOvered = item === _class;
});
console.log(isOvered); // true

// 使用 map 的情况 - 生成新的数组,对外部遍历没有威胁
let newArr = classArr.map(item => item === 'functional');
console.log(newArr); // [ false, false, true ]

函数式编程的原理特点

什么是函数式编程的原理?

对于 原子操作组装 + 合并

  • 加法结合律 | 因式分解 | 完全平方公式 - 原子组合的变化
    • a + b + c = (a + b) + c
  • 水源 => 组合(水管 + 走线) => 浴缸

理论思想

1、函数是第一等公民

  1. 逻辑功能实现的落脚点 - 函数;
  2. (实现 + 拼接) * 函数(都是围绕着函数来做处理);

2、声明式编程

  1. 声明需求 => 语义化;

3、惰性执行

  1. 无缝连接,性能节约;
/* 
  惰性函数
*/

let program = name => {
  if (name === 'c') {
    return (program = () => {
      console.log('progressive');
    });
  } else if (name === 'objective') {
    return (program = () => {
      console.log('objective');
    });
  } else {
    return (program = () => {
      console.log('functional');
    });
  }
};

program('program')();
console.log('lazy');
program();

/* 
  输出结果:
    functional
    lazy
    functional
*/

/* 
  普遍存在性能里面,例如:当页面的某一个逻辑只需要一次加载的时候(权限判断 等)
  例如:program 的一个账号管理的逻辑,第一次执行的时候判断当前是哪个账号逻辑,以后再次执行就不用再次判断了
*/

无状态与无副作用

  • 无状态 - 幂等、数据不可变;

    • 幂等:例如 - 输入和输出相同,例如一个管子输入的是水,输出的也得是水,不能是别的物体;
    • 数据不可变:不可操作改变源数据;
      • 函数内部不能直接操作外部的变量,如果一个函数直接操作了外部变量,那边当需求变动时,其他函数再次操作这个变量就会存在风险;
  • 无副作用 - 函数内部不应该直接对整个系统中任何参数变量做改动;

实际开发

纯函数改造

const _class = {
  name: 'objective'
};

// 函数内部引入外部变量后 —— 违反了无状态
const score = str => _class.name + ':' + str;

// 修改了传入参数 —— 违反了无副作用
// 如果改动的话,需要对 obj 做拷贝,改完之后再 return 出去
const changeClass = (obj, name) => (obj.name = name);

changeClass(_class, 'functional');
console.log(_class); // { name: 'functional' }
let result = score('good!');
console.log(result); // functional:good!

改造后:

const _class = {
  name: 'objective'
};

// 不依赖外部变量
const score = (obj, str) => obj.name + ':' + str;

// 未修改外部变量 —— 解构后,合并
const changeClass = (obj, name) => ({ ...obj, name });

let res1 = changeClass(_class, 'functional');
console.log(res1); // { name: 'functional' }
console.log(_class); // { name: 'objective' }
let res2 = score(_class, 'good!');
console.log(res2); // objective:good!

流水线组装 - 加工 & 组装

加工 - 柯里化

f(x, y, z) => f(x)(y)(z)
const sum = (x, y) => {
  return x + y;
};

sum(1, 2);

// 简单的柯里化后
const add = x => {
  return y => {
    return x + y;
  };
};

add(1)(2);

实现:体系 = 加工 + 组装;单个加工输入输出应当单值化。

**面试题:**书写构造可拆分传参的累加函数

add(1)(2)(3);
// 1、构造柯里化解构
// 2、输入:处理外部的 arguments => 类数组形态处理
// 3、传入参数无限拓展 => 递归 内层逻辑 => 返回函数
// 4、主功能实现 => 累加
// 5、输出

const add = function () {
  // 输入
  let args = Array.prototype.slice.call(arguments); // 把类数组转换成数组

  // 内层处理
  let inner = function () {
    args.push(...arguments); // 内外层参数合并
    return inner;
  };

  // 复写 toString 方法
  inner.toString = function () {
    return args.reduce((prev, cur) => {
      return prev + cur;
    });
  };

  return inner;
};

let res = add(1)(2)(3)(4) + ''; // '10' - 字符串
console.log(res, typeof res);
let res2 = parseInt(add(1)(2)(3)(4)); // 10 - 数字
console.log(res2, typeof res2);

流水线 - 组装函数

// 组装函数
const compose = (f, g) => x => f(g(x));

const sum1 = x => x + 1;
const sum2 = x => x + 1;
const sum12 = compose(sum1, sum2);
let res = sum12(2);
console.log(res); // 4

实际实现使用:

// 命令式
trim(reverse(toUpperCase(map(arr))));

// 面向对象
arr.map().toUpperCase().reverse().trim();

// 函数式
const result = compose(trim, reverse, toUpperCase, map); // 从右往左
pipe(map, toUpperCase, reverse, trim); // 管道流 - 从左往右

BOX 与 函子

扩展内容

class Mail {
  constructor(content) {
    this.content = content;
  }

  map(fn) {
    console.log('fn ========= ', fn);
    /* 
      fn =========  ƒ (mail) {
        console.log('mail ===== ', mail); // love
        return read(mail);  
      }
    */
    console.log('this.content ========= ', this.content); // love
    return new Mail(fn(this.content));
  }
}

// 1、拆开信
let mail1 = new Mail('love');
console.log('mail1 ===== ', mail1); // Mail {content: 'love'}

// 2、读了信
let mail2 = mail1.map(function (mail) {
  console.log('mail ===== ', mail); // love
  return read(mail);
});

// 3、烧了信
let mail3 = mail1.map(function (mail) {
  return burn(mail);
});

// 4、老师查寝时候
mail3.map(function (mail) {
  return check(mail);
});

// 链式的写法
new Mail('LOVE').map(read).map(burn).map(check);