三种编程的范式
命令式,C 语言
面向对象,Java、C++
函数式,Haskell、Clojure
那么,JavaScript是哪种?
三种都是,JavaScript同时包罗了这 3 种编程范式,JavaScript果然是非常自由的语言。
而浏览器使用JavaScript语言的发展也是这个顺序:
我们写原生JS或者使用JQueary的时候我们是命令式
后俩开始使用vue2与react16.8之前(class语法)的时候框架是面向对象的思路
再到vue3与react hooks语法阶段,函数式编程就开始占据上风,占领统治地位了。
所以兄弟们,让我们开始拥抱函数式编程吧!
举个栗子
['john-reese', 'harold-finch', 'sameen-shaw']
// 转换成
[{name: 'John Reese'}, {name: 'Harold Finch'}, {name: 'Sameen Shaw'}]
1. 命令式编程风格
let names = ['john-reese', 'harold-finch', 'sameen-shaw'];
let result = [];
for (let i = 0; i < names.length; i++) {
let parts = names[i].split('-');
let formattedName = parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
result.push({ name: formattedName });
}
console.log(result);
2. 面向对象风格
class NameFormatter {
constructor(names) {
this.names = names;
}
formatNames() {
return this.names.map(name => {
let parts = name.split('-');
let formattedName = parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
return { name: formattedName };
});
}
}
let formatter = new NameFormatter(['john-reese', 'harold-finch', 'sameen-shaw']);
let result = formatter.formatNames();
console.log(result);
3. 函数式风格
const names = ['john-reese', 'harold-finch', 'sameen-shaw']
const capitalize = x => x[0].toUpperCase() + x.slice(1).toLowerCase();
const genObj = (key) => {
return { name: key };
}
let result = names.join(' ').split('-').map(capitalize).map(x => genObj(x))
console.log(result);
大家是不是明显感觉代码清晰,且直观,方便阅读了。重点是函数,通过函数的组合去解决问题。
函数式编程的特点
函数是一等公民,我们基本的操作都是在操作函数。
全程声明式,React是声明式,我们只需要描述UI,以及控制更新,其余交给React;或者像不像SQL语句,我们不知道SQL语句是如何实现的,但是我们知道怎么用。
惰性执行,函数不会提前执行,而是等到真正需要的时候才执行
无状态,一个函数在任何时候输入相同,输出必须相同(带有网络请求或者获取Date的都不行)
其他数据不可变/没有副作用,如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
副作用:一开始看到这个词大家可能觉得这肯定是一个不好的东西,毕竟副作用在我们印象中的感觉是做了多余的事情。而实际上的含义是,在完成函数主要功能之外完成的其他副要功能。而这个副要功能有时候却是必要功能。
函数式编程详解
示例代码
// curry 函数借助 Function.length 读取函数元数
function curry(func, arity=func.length) {
// 定义一个递归式 generateCurried
function generateCurried(prevArgs) {
// generateCurried 函数必定返回一层嵌套
return function curried(nextArg) {
// 统计目前“已记忆”+“未记忆”的参数
const args = [...prevArgs, nextArg]
// 若 “已记忆”+“未记忆”的参数数量 >= 回调函数元数,则认为已经记忆了所有的参数
if(args.length >= arity) {
// 触碰递归边界,传入所有参数,调用回调函数
return func(...args)
} else {
// 未触碰递归边界,则递归调用 generateCurried 自身,创造新一层的嵌套
return generateCurried(args)
}
}
}
// 调用 generateCurried,起始传参为空数组,表示“目前还没有记住任何参数”
return generateCurried([])
}
// 使用展开符来获取数组格式的 pipe 参数
const compose = (...fns) => {
const fnReversed = fns;
return args => {
return fnReversed.reduceRight((ret, fn) => fn(ret), args);
};
};
const pipe= (...fns) => {
const fnReversed = fns;
return args => {
return fnReversed.reduce((ret, fn) => fn(ret), args);
};
};
// 柯里化
// f(a,b,c) → f(a)(b)(c) / f(a)(b,c) / f(a,b)(c)
function add(a, b, c) {
return a + b + c;
}
const addCurry = curry(add);
addCurry(1)(2)(3);// 6
addCurry(1,2)(3);// 6
addCurry(1)(2,3);// 6
// 函数组合
const f = x => x + 1;
const g = x => x * 2;
const fg = compose(f, g);
fg(1) //3
代码挺抽象,大家没看明白没关系,我给大家讲讲
柯里化
柯里化,就是把一个多参数的函数,转化为单参数函数,也叫单元函数。
函数组合,就是把多个函数合并起来组合成一个新的函数。
对比代码
// 命令式的写法
log(toUpperCase(head(reverse(arr))))
// 面向对象的写法
arr.reverse()
.head()
.toUpperCase()
.log()
// 函数组合
compose(log, toUpperCase, head, reverse);
pipe(reverse, head, toUppderCase, log);
函数组合让代码变得简单并且增加了可读性
总结
函数式编程的优点:代码简洁,方便理解,减少BUG
缺点:性能肯定低于命令式辣。