原文链接(格式更好):《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(); // 这里触发整个流程的执行
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) // 函数将不再执行