优雅的高阶函数——JavaScript 函数式编程

226 阅读5分钟

命令式编程与声明式编程

这里提到的命令式与声明式只是一种编程风格。

  • 命令式编程的特点: 只关注如何使用代码得到结果。
  • 声明式编程的特点: 重点描述该做什么,怎么做不是关心的重点。

举个例子就很好理解了

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 NumberObject to Array to StringObject to Array to Boolean
数字[ Number ]{ key: Number }-Number.toStringBoolean(Number)
字符串String.split{ key: String }Number(String)String.replaceBoolean(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个都可以。同样,这样在修改函数调用顺序的时候是不是也方便很多呢?

写在最后,如果感觉通过文章学到知识,还请点个赞呗。也可以关注我哦,不定期的更新一些文章,供大家参考学习,共同进步哦。