1-7 函数式编程

120 阅读7分钟

原文链接(格式更好):《1-7 函数式编程》

函数式编程:鼓励使用纯函数(Pure Functions),即对于相同的输入,始终产生相同的输出,并且没有副作用(没有改变外部状态的行为)

发展历程

命令式 => 面向对象 => 面向函数

面试题:

将数组:['process&%coding', 'object&%coding', 'function&%coding']

转为 JSON:[{ name: 'Process Coding'}, { name: 'Object Coding'}, { name: 'Function Coding'}]

命令式代码举例:

const arr = ["process&%coding", "object&%coding", "function&%coding"];
const parseArr = [];
const Parser = (arr, parseArr) => {
  arr.map((item) => {
      const newItem = [];
      item.split("&%").map((_item) => {
      newItem.push(_item[0].toUpperCase() + _item.slice(1));
      });
      parseArr.push({ name: newItem.join(" ") });
  });
};

Parser(arr, parseArr);

// 存在的问题
// 1. 有包裹逻辑 - 需要看完整段代码才能明白是在做啥
// 2. 存在临时变量,并且首尾封闭 - 拓展/返回临时变量的难度更高

对象式代码举例:

const Parser = {
  toJson(arr, parseArr) => {
    arr.map((item) => {
        const newItem = [];
        item.split("&%").map((_item) => {
        newItem.push(_item[0].toUpperCase() + _item.slice(1));
        });
        parseArr.push({ name: newItem.join(" ") });
    });
  };
}

Parser.toJson(arr, parseArr);

函数式代码举例:

// 需求拆解:
// 1. 数组转为 JSON:arrToJSON
// 2. arrToJSON = stringFormat + objHelper
// 3. stringFormat = split + capitalize + join

const arr = ["process&%coding", "object&%coding", "function&%coding"];

// 原子逻辑:
// objHelper: 生成对象
const createObj = (key, anyValue) => {
  const obj = {
    [key]: anyValue
  }
  return obj
}

// strSplit: 按需分割
const strSplit = (str, splitKey) => str.split(splitKey)
  
// capitalize: 首字母大写
const capitalize = str => {
  return str[0].toUpperCase() + str.slice(1)
}

// 逻辑拼装:
// stringFormat: 字符串处理
const stringFormat = str => {
  return strSplit(str, '&%').map(item => capitalize(item)).join(' ')
}

const objHelper = (str) => {
  return createObj('name', str)
}

// arrToJSON: 生成 JSON
const arrToJSON = (arr) => {
  return arr.map(item => objHelper(stringFormat(item)))
}

// 好处:
// 1、将复杂的逻辑通过合理的拆分,变成简单功能的拼装,利于后期的维护与扩展

函数式编程的原理、特点

什么是函数式编程?

将复杂功能“因式分解”,然后使用“加法结合律”组装完成功能的思维形式

理论思想

函数是一等公民

每个原子功能,就是一个采用命令式编程的函数,并且属于惰性执行(需要的时候才会执行)

惰性执行

// 非惰性
function createPersonNoLazy() {
  console.log("[ createPersonNoLazy person is created ] >");
  return {
    name: "xx",
    age: 30,
  };
}
// 每次执行都重新生成新的
const preson1 = createPersonNoLazy();
const preson2 = createPersonNoLazy();

// 惰性
function createPersonLazy() {
  console.log("[ createPersonLazy person is created ] >");
  const person = {
    name: "xx",
    age: 30,
  };

  // 函数重写
  createPersonLazy = () => {
    console.log("[ createPersonLazy person is existed ] >");
    return person;
  };

  return person;
}

// 第一次执行都生成新的,后续直接返回 person
const person3 = createPersonLazy();
const person4 = createPersonLazy();

函数式编程的要求

无状态

指函数在执行时不依赖或修改外部状态。它的行为仅由输入参数决定,并且对于相同的输入,总是产生相同的输出,不受外部环境的影响。

无副作用

指函数在执行过程中不对外部环境产生可观察的影响,即不会对输入的参数、外部变量]进行修改。

函数式编程的实际开发

1. 纯函数改造

满足无状态 && 无副作用的函数就是纯函数

const a = 1

// 引入了外部变量,违反了无状态
const add = x => a + x

const add = (a, x) => a + x // 无状态

// 改变了参数/外部变量,违反了无副作用
const add = obj => obj.x++

const add = obj => { ...obj, x: obj.x + 1} // 无副作用

a. 函子

定义:一个类/构造函数,具有map方法,每次调用map会生成新的对象

const Box = function(val) {
    this.val = val;
}

// of 函数:让使用者不必使用 new 来新建对象
Box.of = function(val) {
    return new Box(val);
}
Box.prototype.map = function(func) { // 必须
    return this.isNull() ? Box.of(null) : Box.of(func(this.val));
}

// 错误处理-函子
Box.prototype.isNull = function() {
  return this.val === null || this.val === undefined 
  
}

const add = x => x + 1
const square = x => x * x
const setNull = () => null

Box.of(1).map(add).map(setNull).map(square).val === null // true

b. 函子的作用:适合消除副作用

class Monad {
  constructor(value) {
    this.value = value;
  }

  bind(transform) {
    // `bind` 方法用于将当前 Monad 的值传递给一个函数(transform),并返回一个新的 Monad
    return transform(this.value);
  }

  // `value` 方法用于获取 Monad 的值
  value() {
    return this.value;
  }
}

// 使用示例
const readFile = function (filename) {
  const content = fs.readFileSync(filename, "utf-8");
  return new Monad(content);
};

const print = function (x) {
  console.log(x);
  return new Monad(x);
};

const tail = function (x) {
  const lastLine = x.split('\n').pop();
  return new Monad(lastLine);
};

// 链式操作
const monad = readFile('./xxx.txt').bind(tail).bind(print);
// 执行操作
monad.value();  // 这里触发整个流程的执行

图解 Monad - 阮一峰的网络日志

2. 加工 & 组装

a. 加工 - 柯里化

// 未柯里化
const add1 = (x, y, z) => x + y + z
add(1, 2, 3)

// 柯里化
const add2 = (x) => {
  return y => {
    return z => x + y + z
  }
}
add2(1)(2)(3)

// 柯里化转换函数
const toKLH = (fn) => {
  const KLH = (...arg) => {
    if (arg.length === fn.length) {
      return fn(...arg);
    } else {
      return (...arg2) => {
        return KLH(...arg.concat(arg2));
      };
    }
  };

  return KLH;
};

// 分批次使用
const add10 = toKLH(add1)(10) // 计算初始值为 10 的加法
const add20 = toKLH(add1)(20) // 计算初始值为 20 的加法

add10(1)(3)
add20(1)(3)

为什么需要柯里化,为了函数的输入输出单值化(单元函数,更利于组合),更加方便操作多值函数

b. 组装 - 高阶函数

const sum1 = x => x + 1
const sum2 = x => x + 2 

// 函数式
const compose = (f, g) => {
  return x => {
    return f(g(x))
  }
}
const sum1_2 = compose(sum1, sum2)(value) // 4

// 命令式
sum2(sum1(value))()

// 对象式
valueInstance.sum1().sum2()

补充知识

高阶函数

定义:函数为参的函数

黑话:逻辑外壳

面试题

1. 如何使用正确的遍历

数组:for、find、findIndex、forEach、map、filter、reduce、sort、some、every

对象:for in

类数组:for

可遍历:for、for of

a. 为什么数组有这么多的遍历方法?

  • for:通用
  • find:找到某个值
  • findIndex:找到某个值的下标
  • forEach:遍历进行逻辑处理
  • map:生成新数组,顺带进行逻辑处理
  • filter:过滤满足条件的值,并生成新数组
  • reduce:累积
  • sort:排序
  • some:是否 >=1 个满足条件
  • every:是否所有满足条件

本质逻辑是:满足函数式编程,让每个函数有自己应该做的事情

2. JS里面的副作用函数有哪些?

  • split:不会改变原数据
  • slice:不会改变原数据
  • splice:会改变原数据
  • pop:会改变原数据
  • push:会改变原数据
  • shift:会改变原数据
  • unshift:会改变原数据
  • reverse:会改变原数据
  • sort:会改变原数据
  • ...

3. 柯里化

// 实现一个 add(1)(2)(3)...(n)() 的累加函数
const add = (...args1) => {
  const inner = (...args2) => {
    if (args2.length) {
      args1.push(...args2);
      return inner;
    } else {
      return args1.reduce((total, curr) => (total += curr), 0);
    }
  };

  return inner;
}

add(1)() // 1
add(1)(2)() // 3
add(1)(2)(3)() // 6
add(1)(2)(3)(4)() // 10
add(1, 2, 3, 4)() // 10

a. 柯里化与闭包的关系

“孪生子”

闭包定义:返回函数的函数,其中内部函数使用了外部函数定义的变量,形成了闭包

柯里化定义:将多参数的函数转为接受单/部分参数的函数,并且返回接受剩余参数和返回结果的函数

// 闭包 ----
const add10 = () => {
  const num = 10
  return (x) => x + num
}
const addInit = add10()
addInit(10) // 10+10,20

// 柯里化 ----
// 初始函数 add
const add1 = (x, y) => {
  return x + y
}
add1(10, 10)

// 初始函数 add 柯里化后
const add2 = (x) => {
  return y => {
    return x + y
  }
}
add2(10)(10)

4. 柯里化的运用

a. 防抖、节流

ⅰ. 防抖

定义:触发后,x 时间后才生效(每次触发都重新计时),适用于:onresize、输入框搜索等

const debounce = (fn, delay) => {
  delay = delay || 200
  let timer = null
  return function() {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arguments)
    }, delay)    
  }
}

let count = 0
window.onresize = debounce((e) => {
  console.log('e.type', e.type)
  console.log('count', ++count)
}, 500)

ⅱ. 节流

定义:x 时间内只会触发一次

 const throttle = (fn, delay) => {
  delay = delay || 200;
  let timer = null;
  return function () {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
};

let count = 0
window.onresize = debounce((e) => {
  console.log('e.type', e.type)
  console.log('count', ++count)
}, 1000)

b. 缓存计算

// 需求:大数据的计算
const calculateFn = (num)=>{
  const startTime = new Date()
  for(let i=0;i<num;i++){
    // 大数计算.....
  } 
  const endTime = new Date()
  console.log(endTime - startTime)
  return "Calculate big numbers"
}

calculateFn(10_000_000_000) // 耗时 8s
calculateFn(10_000_000_000) // 每次调用都耗时 8s
calculateFn(10_000_000_000) // 每次调用都耗时 8s

// 柯里化-缓存改造
const caches = (fn) => {
  const cacheResult = {};
  return function (num) {
    if (!cacheResult[num]) {
      cacheResult[num] = fn(num);
    }
    return cacheResult[num];
  };
};

const calculateCacesFn = caches(calculateFn);

calculateCacesFn(10_000_000); // 首次调用,耗时 8s
calculateCacesFn(10_000_000); // 重复调用,直接拿值
calculateCacesFn(20_000_000); // 首次调用,耗时 8s
calculateCacesFn(20_000_000); // 重复调用,直接拿值

c. 实际业务

业务需求:某个函数调用 n 次后,不再调用

const beforDo = (deNum, fn) => {
	let count = 0
  let result
  return function() {
    if(count < deNum) {
      result = fn.apply(this, arguments)
      count++
    }
    return result
  }
}
const fn = beforDo(3, (x) => console.log(x))
fn(1) // 1
fn(2) // 2
fn(3) // 3
fn(4) // 函数将不再执行