命令式编程与声明式编程
这里提到的命令式与声明式只是一种编程风格。
- 命令式编程的特点: 只关注如何使用代码得到结果。
- 声明式编程的特点: 重点描述该做什么,怎么做不是关心的重点。
举个例子就很好理解了
const str = "Louie Butch is a boy";
let strNew1 = "";
let strNew2 = "";
// 命令式编程
for (let i = 0; i < str.length; i++) {
if (str[i] === " ") {
strNew += "-";
} else {
strNew += str[i];
}
}
// 声明式编程
strNew2 = str.replace(/ /g, "-");
示例中,使用 str.replace,通过正则表达式把所有空格替换成连字符,也就是在描述这是一个替换的操作,具体如何处理空格则不明确。而对于命令式编程而言,我们可以很清晰的看到空格是怎么一步一步的被替换成连字符的。命令式编程需要大量时间去理解时间,去理解整个过程的来龙去脉,这个过程也是比较繁杂。
函数式编程的核心概念
对于上述示例,我们能比较明显的区分声明式与命令式。其中的优劣,也不用再提。 对于函数式编程(声明式编程)的核心概念:不可变性、纯函数、数据转换、高阶函数和递归。
不可变性
应用中我们不直接更改原始数据结构,而是创建数据结构副本,所有操作都使用副本。我们举个例子:
const list = [{ title: "Rad Red" }, { title: "Lawn" }, { title: "Party Pink" }];
const addColor = (title, colors) => {
colors.push({ title });
return colors;
};
const newList = addColor("Glam Green", list);
console.log(newList.length); // 4
console.log(list.length); // 4
const list = [{ title: "Rad Red" }, { title: "Lawn" }, { title: "Party Pink" }];
const addColor = (title, colors) => colors.concat({title});
const newList = addColor("Glam Green", list);
console.log(newList.length); // 4
console.log(list.length); // 3
从上面的示例中,我们可以明白,在定义一个函数时,应该保持原始数据的不可变性。
总结几个常用的本科变性的拓展:
-
数组的不可变性
- Array.concat();
- 数组的合并:[...Array, element];
- Array.map();
- Array.filter();
-
对象的不可变性
- Object.assign({}, Object);
- 对象的合并:[...Object, { key: value }];
纯函数
纯函数指基于一个参数做结算并返回一个值的函数。至少接受一个参数并始终返回一个值或函数,这种函数没有副作用,没有全局变量,不改变应用状态。因此,纯函数也可以看成是参数不可变的函数,也正因为有以上特性,则纯函数也可以轻而易举地测试。
举个例子:
const louie = {
name: "Louie Butch",
canRead: false,
canWrite: false
};
const selfEducate = person => ({
...person,
canRead: true,
canWrite: true
});
console.log(selfEducate(louie));
console.log(louie);
// { name: "Louie Butch", canRead: true, canWrite: true }
// { name: "Louie Butch", canRead: false, canWrite: false }
通过这个例子我们应该比较清楚的知道该怎么去写一个纯函数了。
由此我们建议在编写函数时遵守如下规则:
- 函数应该至少接受一个参数。
- 函数应该返回一个值或另一个函数。
- 函数不应该更改任何参数。
数据转换
在函数式编程中,数据从一种格式变成另一种格式,也就是我们使用函数将原数据转换成数据副本。下面将罗列几种常用的数据转换函数。
- Array.join: 从数组中提取出以指定符号分隔的字符串。
- Array.filter: 根据原数组产出一个新数组。只接受一个参数(断言),它将在数组的每个元素上调用断言,元素作为参数传给断言,返回值决定是否将元素添加到新建的数组中。
- Array.map: 接受一个函数参数,这个参数函数在每个元素上调用一次,不管返回什么都将添加到新数组中。
- Object.keys: 从对象中获取所有键,返回由键构成的数组。
- Array.reduce/Array.reduceRight: 把数组转换成任何值,包括数字、字符串、布尔值、对象甚至是函数。Array.reduce 从数组开头开始归约,而 Array.reduceRight 反之。
Array.filter 中提到的断言: 一个始终返回布尔值的函数。要删除数组中的元素,不建议使用Array.pop, Array.splice,应该使用 Array.filter,因为它执行的是不可变操作。
下边整理了数据间相互转换的方法:
| 数组 | 对象 | 数字 | 字符串 | 布尔值 | |
|---|---|---|---|---|---|
| 数组 | Array.filter Array.map Array.reduce Array.reduceRight | Array.reduce Array.reduceRight | Array.reduce Array.reduceRight | Array.reduce Array.reduceRight Array.join | Array.reduce Array.reduceRight |
| 对象 | Object.keys 和 Array.map | { ...Object } | Object to Array to Number | Object to Array to String | Object to Array to Boolean |
| 数字 | [ Number ] | { key: Number } | - | Number.toString | Boolean(Number) |
| 字符串 | String.split | { key: String } | Number(String) | String.replace | Boolean(String) |
| 布尔值 | [ Boolean ] | { key: Boolean } | Boolean(Number) | Boolean(String) | - |
高阶函数
高阶函数指用于处理其他函数的函数,参数可以是函数,也可以返回函数。上面我们提到的 Array.map、Array.filter、Array.reduce 等都是高阶函数。
举个柯里化(Currying)的例子:
const userLogs = userName => message => console.log(`${userName} -> ${message}`);
const log = userLogs("Louie Butch");
log("attempted to load 20 fake members");
getFakeMembers(20).then(
members => log("successfully Loaded");
error => log("encountered an error");
);
// Louie Butch -> attempted to load 20 fake members
// Louie Butch -> successfully Loaded
// Louie Butch -> encountered an error
userLogs 是高阶函数。log函数是由 userLogs 得来的,每次调用 log 函数都会在消息前面加上 “Louie Butch”。针对于柯里化,后续会去更文解读一下。
递归
递归指创建重新调用自身的函数。如果涉及到循环,那么递归函数往往能派上用场。
const countdown = (val, fn) => {
fn(val);
return value > 0 ? countdown(countdown - 1, fn) : val;
}
countdown(5, val => console.log(val));
// 5,4,3,2,1
const butch = {
type: "person",
data: {
gender: "male",
info: {
age: 22,
fullname: {
first: "Butch",
last: "Louie"
}
}
}
};
const deepPick = (fields, obj = {}) => {
const [firstField, ...remaining] = fields.split(".");
return remaining.length ? deepPick(remaining.join("."), obj[firstField]) : obj[firstField];
}
deepPick("type", butch); // people
deepPick("data.info.fullname.first", butch); // butch
deepPick 函数要么返回一个值要么调用自身,最终还是会返回一个值。 递归技术适合在搜索数据结构中使用。可以递归迭代子文件夹,可以递归迭代 HTML DOM。
合成
函数式程序是把逻辑拆分成一系列关注特定任务的小型纯函数,最终由把这些小型函数整合到一起,最终形成一个综合应用。归纳总结一下:JavaScript函数式编程就是编写优雅的高阶函数
const complate = "YYYY-MM-DD hh:mm:ss tt";
const civilianDays = date => date.replace("YYYY", "2023").replace("MM": "08").replace("DD", "03");
const civilianHours = date => date.replace("hh", "00").replace("mm": "00").replace("ss", "00");
const appendAMPM = date => date.replace("tt", "AM");
const both = date => appendAMPM(civilianHours(civilianDays(date)));
const dateTime = both(complate);
console.log(dateTiem); // 2023-08-03 00:00:00 AM
当你在维护一份代码时发现 appendAMPM(civilianHours(civilianDays(date))) 这样一串代码是否有一种头大的感觉?这时候,优雅的高阶函数就体现出价值了。
const complate = "YYYY-MM-DD hh:mm:ss tt";
const civilianDays = date => date.replace("YYYY", "2023").replace("MM": "08").replace("DD", "03");
const civilianHours = date => date.replace("hh", "00").replace("mm": "00").replace("ss", "00");
const appendAMPM = date => date.replace("tt", "AM");
const compose = (...fns) => arg => fns.reduce((composed, f) => f(composed), arg);
const both = compose(
civilianDays,
civilianHours,
appendAMPM
);
const dateTime = both(complate);
console.log(dateTiem); // 2023-08-03 00:00:00 AM
精髓就在 const compose = (...fns) => arg => fns.reduce((composed, f) => f(composed), arg); 这一段代码中,反复思考,理解这种用法。这种方法易于扩展,随时都可以在 compose中添加更多的函数,10个,20个都可以。同样,这样在修改函数调用顺序的时候是不是也方便很多呢?
写在最后,如果感觉通过文章学到知识,还请点个赞呗。也可以关注我哦,不定期的更新一些文章,供大家参考学习,共同进步哦。