一步一步教你 JavaScript 函数式编程(第三部分)

这是关于使用 JavaScript 进行函数式编程的最后一篇文章,我们将从 第1部分第2部分 开始,将 JSON 输出最终输出为满足我们需求的对象 :

  • (已经完成) 过滤掉一个月前发布(比如说,30天)的文章。
  • (已经完成) 通过文章的标签(tags)对文章进行分组(这意味着如果文章有多个标签,那么该文章会出现在多个分组中)。
  • (本文讨论) 按发布日期(published)降序排序每个标签文章列表。

现在我们有一个被过滤并按标签分组的对象,我们需要为每个标签组进行一些排序。运行我们的 app.js 程序后(注,app.js代码可以在这个 gist 中查看),我们输出的对象现在是这样的:

{
    'destructuring': [
    { id: 2, title: 'ES6 Promises', tags: ['es6', 'promises'], /*...*/ },
    { id: 4, title: 'Basic Destructuring in ES6', tags: ['es6', 'destructuring'], /*...*/ },
],
    'es6': [ /*...*/ ],
    /*...*/
}

我们需要对每个标签组的记录数组按照发布日期降序(最新到最旧)顺序进行排序。

请在这个 gist 中查看完整源代码(也包括这篇文章的代码)。

列表排序

JavaScript 通过 Array.prototype.sort() 使列表排序相当容易。 由于没有比较函数参数,.sort() 将尝试将元素转换为字符串,并以 Unicode 编码顺序进行排序。它还对数组进行排序。我们希望我们的排序是一个 纯函数,不希望改变我们排序的数组;但是返回一个新排序的数组。

纯函数 不依赖于且不改变其本身作用域之外的变量状态的函数。如果给定相同的参数,它们将始终返回相同的结果。(愚人码头注:也就是说,纯函数的返回值只由它调用时的参数决定,它的执行不依赖于系统的状态(比如:何时、何处调用它——译者注)。 纯函数是函数式编程的一个基础。)

我们创建一个排序函数,它将使用列表和比较函数作为参数,并返回一个新的排序列表。如图:

一步一步教你 JavaScript 函数式编程(第三部分)

function sort(list, fn) {
    return [].concat(list).sort(fn);
}
var sortBy = rightCurry(sort); // right curried version

在这里,我们只是使用 Array.prototype.concat() 来创建一个新的浅拷贝数组,然后对这个数组进行排序并返回。比较函数通过 Array.prototype.sort() 一次传递两个对象, ab,并且应该执行以下操作:

  1. 如果 a < b,返回 -1 或小于 0(排序将使 a 的索引低于 b
  2. 如果 b < a,返回 1 或大于0(排序将使b 的索引低于a
  3. 如果 a == b,返回 0(排序不会改变 ab 的索引)

我们来尝试一些列表:

var numbers = [5,1,3,2,4],
    names = ['River','Zoë','Wash','Mal','Jayne','Book','Kaylee','Inara','Simon'];
function asc(a,b) {
    return a < b ? -1 : (b < a) ? 1 : 0;
}
sortBy(asc)(numbers);
// [1, 2, 3, 4, 5]
sortBy(asc)(names);
// ["Book", "Inara", "Jayne", "Kaylee", "Mal", "River", "Simon", "Wash", "Zoë"]

注意:namesnumbers 未改变。

创建通用的比较函数

Array.prototype.sort() 接受的比较函数有一个非常特殊的接口,这在其他地方不是很方便。如果我们可以创建通用的比较函数,那就更好了。二元比较函数返回 truefalse ,并将它们作为Array.prototype.sort() 的比较函数进行重用。

我们需要的是一个高阶函数,它可以接受我们的二元比较函数,返回一个布尔值,并给我们一个新的函数,它可以返回 -101来满足我们需要的接口。

原来,我们可以很容易地做到这一点。 我们称它为 comparator(),它采用了一个返回 truefalse 的二元比较函数,并返回一个比较函数,该函数保存在 Array.prototype.sort() 定义的 API 中。

// 修改简单的二元比较函数
// 以使用常规的 sort() 返回预期的 `-1`,`0`,`1` 返回值。
function comparator(fn) {
    return function(a,b) {
        return fn(a,b) ? -1
            : fn(b,a) ? 1
                : 0;
    };
}

现在我们来看看如何使用我们以前的示例数据。

var numbers = [5,1,3,2,4],
    names = ['River','Zoë','Wash','Mal','Jayne','Book','Kaylee','Inara','Simon'];
// 通用的二元比较函数,返回 true|false
function lessThan(a,b) { return a < b; }
// 创建比较函数
var asc = comparator(lessThan);
sortBy(asc)(numbers);
// [1, 2, 3, 4, 5]
sortBy(asc)(names);
// ["Book", "Inara", "Jayne", "Kaylee", "Mal", "River", "Simon", "Wash", "Zoë"]

这展示了使用高阶函数的另一个好处:我们可以使用函数,并创建新的函数来完成更复杂或更具体的任务。

高阶函数是指将函数作为参数 和/或 将函数作为返回值的函数。

比较对象属性

到目前为止,我们的解决方案对于包含数字和字符串等标量值的数组非常有用。但是我们要处理的是对象数组。我们如何使用我们的通用的比较函数以及 comparator() 根据给定的属性来对对象列表进行排序呢?

幸运的是,我们已经使用过 useWith() 高阶函数在 上一篇文章中 解决过这个问题。

useWith() 允许我们通过传递另一个函数来修改发送给任何函数的参数。在本例中,我们要确保传递给我们的比较函数的两个对象通过我们定义的另一个以前的函数 getWith('published'),以便只比较每个对象上 published 属性。

让我们来看看这个实例如何与一些示例代码一起使用:

var kessel_times = [
    { name: 'Slave 1', parsecs: 13.17 },
    { name: 'Falcon', parsecs: 11.5 },
    { name: 'Executor', parsecs: 18 }
];
var ascendingByParsecs = useWith(comparator(lessThan), getWith('parsecs'), getWith('parsecs'));
sortBy(ascendingByParsecs)(kessel_times);
// [
// { name: "Falcon", parsecs: 11.5 },
// { name: "Slave 1", parsecs: 13.17 },
// { name: "Executor", parsecs: 18 }
// ]

我自己做了很多特别修改…

按发布日期排序我们的分组

现在,我们有了对输出进行排序的工具。我们需要迭代输出对象 mapObject 中的分组,对于每个分组,都是一个列表,按照每个对象的发布日期( sortBy + comparator + useWith ),从最新到最老的顺序进行排列。

让我们来看一下:

function greaterThan(a,b) {
    return a > b;
}
var descending = comparator(greaterThan),
    descendingByPublishDate = useWith(descending, getWith('published'), getWith('published'));
// 获取 group 分组下文章记录列表
function sortByPublishDate(group, recs) {
    return sortBy(descendingByPublishDate)(recs);
}
var finished = mapObjectWith(sortByPublishDate)(finalgroups);

从我们上一篇文章中获取的输出对象,是一个具有属性名与标签匹配的对象,每个对象包含一个未排序的文章记录列表;而这里返回的对象,格式上是相同的,但是已经根据 published 属性对记录列表进行了排序。

{
    'es6': [
    { id: 7, displayDate: "2015-08-06", published: 1438876909394, ... }
    { id: 2, displayDate: "2015-07-26", published: 1437926509394, ... },
    { id: 9, displayDate: "2015-07-06", published: 1436205701255, ... }
],
    'functional programming': [
    { id: 10, displayDate: "2015-08-26", published: 1440604909394, ... },
    { id: 1, displayDate: "2015-07-25", published: 1437847125528, ... }
],
...
}

除了我们在函数库中创建的助手函数之外,在我们的 app.js 中,以下是函数调用的主要顺序,用于获取初始输入并创建最终输出:

var filtered = filterWith(within30Days)(records);
var bytags = pairWith(getWith('tags'))(filtered);
var groupedtags = groupBy(getWith(1), bytags);
var finalgroups = mapObjectWith(getPostRecords)(groupedtags);
var finished = mapObjectWith(sortByPublishDate)(finalgroups);

这是一组相当具有描述性的函数集;并且大多数都是纯函数和高阶函数,可用于处理其他输入或数据。实际上这个应用程序只有两个具体的功能,即 getPostRecords()sortByPublishDate() —— 所有其他函数都是通过可重用的二元比较函数 和 构建函数式实用工具库相结合来完成的。

合成和排序函数

在我们的应用程序中你会注意到的一点,那就是我们不断地执行以下操作:

  1. 拿一个输入列表或对象
  2. 将其传递给一个函数,来创建输出
  3. 获取该输出并将其用作输入(转到步骤2)

这是一个 序列 或 pipeline ,在那里我们有一系列函数,可以输入,处理它,并将其输出传递给系列中的下一个函数。这类似于 UNIX 命令行及其 | (pipe)操作符将上一个命令的输出挂接到下一个命令的输入。

$ ps aux | grep "root"

我们可以在这里做同样的事情,这通常被称为函数式编程中的合成。例如,使用一个函数 g() 并将输出传递给函数 f() ,类似于调用 f(g()) 一样。在本例中,我们在输入端调用函数 g(),输出被传递给函数 f() 作为其参数或输入。

这是一个简单的函数合成,如上所述:

// 合成函数 f() 和 g(), 类似于 f(g()) - f of g
function compose(f, g) {
    return function() {
        var args = [].slice.call(arguments);
        return f(g.apply(this, args));
    }
}

这个简单的 compose() 函数使用两个函数作为参数,并返回一个函数,当执行时,使用输入调用第二个函数,然后使用该调用的结果再调用第一个函数。

function add1(val) {
    return val + 1;
}
function square(val) {
    return val * val;
}
// 从右到左合成两个函数
compose(square, add1)(4);
// 25

但是,在我们当前的博客文章中,我们有多个函数来处理我们的输入。我们需要一个更灵活的compose(),它可以将两个以上的函数作为参数链接在一起。

// 合成: f(g(x)) 参数数量可变(递归)
function compose() {
    var args = [].slice.call(arguments),
        fn = args.shift(),
        gn = args.shift(),
        fog = gn ? function() { return fn(gn.apply(this, arguments)); } : fn;
    return args.length ? compose.apply(this, [fog].concat(args)) : fog;
}

请注意,compose() 从右到左将传入的行数链接在一起。这是标准的,调用 compose(f, g) 等同于 f(g()),从右到左,从内到外的顺序。

如图所示:

一步一步教你 JavaScript 函数式编程(第三部分)

但是还有一种叫做 pipeline(管道),同样把参数函数连接在一起,但是在左侧输入,在右边获得输出,这样传递函数的顺序就更有意义了。

让我们创建一个 pipeline() 函数,它与 compose() 做同样的工作,但是在我们的上下文中,参数顺序颠倒了,是从左到右读取。

如图所示:

一步一步教你 JavaScript 函数式编程(第三部分)

var pipeline = flip(compose);

So easy!使用高阶函数非常容易做到这一点。现在,让我们在 app.js 中调用之前的调用,并构建一个函数,它将链接每个函数的输入和输出。

var mostRecentByTagOrderedByPublishDate = pipeline(
    filterWith(within30Days),
    pairWith(getWith('tags')),
    groupBy(getWith(1)),
    mapObjectWith(getPostRecords),
    mapObjectWith(sortByPublishDate)
);
var composed_finished = mostRecentByTagOrderedByPublishDate(records);

如果我们比较一下合成函数的输出,你会看到它与以前的输出相匹配。现在,您还可以看到为什么我们为每个实用函数创建了 right 柯里化版本。

拥有 right 柯里化版本,其中要操作的数据作为第二,最右和最后一个参数传递,允许我们构建偏应用函数,我们可以将这些函数传递给我们的 compose()pipeline() 高阶函数,以创建更复杂的函数。

小结

对于未接触过函数式编程的前端开发工程师来说,这 3 篇文章理解起来比较困难,甚至说烧脑(对我个人而言)。这 3 篇文章涵盖了很多话题,从左、右Currying(柯里化) ,高阶函数,如 mapfiltersort,甚至使用 compose()pipeline() 来进行函数合成。这 3 篇文章的想法是为了展示如何在实际的开发中使用函数式编程技术来处理数据和构建应用程序。

我们已经深入讨论了这些主题,我希望你能够更好地理解 JavaScript 中的函数式编程;但更重要的是,在你自己的开发过程中使用它们。

JavaScript 函数式编程系列文章

相关阅读资源

英文原文:http://www.datchley.name/getting-functional-with-javascript-part-3/

原文链接:http://www.css88.com/archives/7829

发表评论

登录后才能评论