主讲:云隐
函数式编程的出现
发展历程:命令(脚本)式 => 面向对象式 => 函数式编程
问题的出现 - 从一道面试题开始
面试题:上接浏览器原理 - 参数 parser
- 数组在
url找那个展示形式:location.search获取:?name[]=progressive$%coding&name[]=objective$%coding&name[]=functional$%coding;(其中$%是跟后台约定好的空格表示)
- 参数提取拼接成数组:
['progressive$%coding', 'objective$%coding', 'functional$%coding']
- 手写方法,转换成数组对象:
[{ 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' }
]
*/
- 这种写法是常规的解题思路 - 面向过程;
- 问题:
- 过程中存在逻辑包裹 - 看完整段代码,才能明白在做啥;
- 存在临时变量,并且首尾封闭 - 迭代拓展难度高;
解决方案
step1:需求分析- 需要把 数组 转换成 数组对象;
[字符串 变成 对象]:['progressive$%coding'] => [{ name: 'Progressive Coding'}]- 针对本题:
nameParse => [objHelper(对象处理器) :: strng > object]
step2:模块功能明确objHelperobjHelper = 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、函数是第一等公民
- 逻辑功能实现的落脚点 - 函数;
- (实现 + 拼接) * 函数(都是围绕着函数来做处理);
2、声明式编程
- 声明需求 => 语义化;
3、惰性执行
- 无缝连接,性能节约;
/*
惰性函数
*/
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);