【问题标题】:How to re-create Underscore.js _.reduce method?如何重新创建 Underscore.js _.reduce 方法?
【发布时间】:2015-07-28 10:42:12
【问题描述】:

出于教育目的,我试图重新创建 Underscore.js 的 _.reduce() 方法。虽然我能够使用 for 循环以明确的方式做到这一点。但这远非理想,因为它会改变作为参数提供的原始列表,这是危险的。

我还意识到使用函数式编程风格创建这样的方法更难,因为不可能显式设置 i 值以进行循环。

// Explicit style
var reduce = function(list, iteratee, initial) {
if (Array.isArray(list)) {
    var start;
    if (arguments.length === 3) {
        start = initial;
        for (var i = 0; i < list.length; i++) {
            start = iteratee(start, list[i], i);
        }
    } else {
        start = list[0];
        for (var i = 1; i < list.length; i++) {
            start = iteratee(start, list[i], i);
        }
    }
}
if (list.constructor === Object) {
    var start;
    if (arguments.length === 3) {
        start = initial;
        for (var key in list) {
            start = iteratee(start, list[key], key);
        }
    } else {
        start = list[Object.keys(list)[0]];

        // Delete the first property to avoid duplication.
        delete list[Object.keys(list)[0]];
        for (var key in list) {
            start = iteratee(start, list[key], key);
        }
    }
}

return start;
};

让我苦恼的是,当我的 reduce() 提供了一个参数 initial 时,我需要随后跳过或删除参数的第一个 elementproperty 以用于将返回的最终值。因为不这样做会重复计算第一个元素/属性。在创建具有函数式编程风格的函数时,我想不出我怎么能做这样的事情,涉及_.each()forEach()

这是我的 reduce() 的功能样式,它正在部分工作。提供memo(initial value) 时它可以正常工作,因为我不需要跳过第一个元素/属性。但是当没有提供 memo 时它不能正常工作,因为那时我将 memo 设置为第一个元素或属性,我应该能够在循环期间跳过它,我不知道如何。

// Functional style (not working without memo)
var reduce = function(list, iteratee, memo) {
    var memo = memo || list[0] || list[Object.keys(list)[0]];
    _.each(list, function(element, index, list){
        memo = iteratee(memo, element, index, list);
    });
    return memo;
};

我花了很长时间在 Google 上搜索我的问题的答案。但是没能找到。我非常感谢您的建议。谢谢。

最后,这是我想出的一个附加代码,它不起作用,但我认为它应该。

var reduce = function(list, iteratee, memo) {
    var collection = list;
    var accumulation;

    _.each(collection, function(item){
        if (arguments.length < 3) {
            if (Array.isArray(collection)) {
                accumulation = collection[0];
                collection.shift();
                accumulation = iteratee(accumulation, item);
            } else {
                accumulation = collection[Object.keys(collection)[0]];
                delete collection[Object.keys(collection)[0]];
                accumulation = iteratee(accumulation, item);
            }
        } else {
            accumulation = memo;
            accumulation = iteratee(accumulation, item);
        }
    });

    return accumulation;
};

【问题讨论】:

  • 你可能不知道普通的旧 JavaScript already comes with reduce() for arrays,那么为什么不开始使用它而不是滚动大量 for 循环呢?
  • 如果 OP 需要支持 IE 8,那么他们将无法访问 Array.prototype.reduce()。此外,OP 提到这是一项学术活动。
  • 感谢迈克的意见。我知道原生数组方法reduce()。我没有使用它,因为我想了解它是如何组成的,而且我想让它也支持一个 Object 作为它的列表参数。

标签: javascript functional-programming underscore.js reduce


【解决方案1】:

首先,当您在传递给_.each 的函数中时,您需要一种方法来确定reduce 是否收到初始memo 值。你可以通过多种方式做到这一点。一种方法是简单地根据arguments 的长度设置一个标志。您需要在_.each 调用之外执行此操作,因为您传递给_.each 的函数将有自己的arguments 对象,为reduce 屏蔽arguments 对象。

以您的代码为起点:

var reduce = function(list, iteratee, memo) {
    var considerFirst = arguments.length > 2;
    var memo = memo || list[0] || list[Object.keys(list)[0]];
    _.each(list, function(element, index, list){
        if (index > 0 || considerFirst) {
            memo = iteratee(memo, element, index, list);
        }
    });
    return memo;
};

不过,这仍然不太正确。我们还需要更新您默认memo 的方式。目前,如果memo 接收到一个假值(例如0),我们仍然将其设置为列表中的第一个元素,但我们没有设置指示忽略第一个元素的标志。这意味着reduce 将处理第一个元素两次。

要做到这一点,您需要更改默认 memo 的方式,仅在未传入参数时进行设置。您可以执行以下操作:

var reduce = function(list, iteratee, memo) {
    var considerFirst = true;
    if (arguments.length < 3) {
        memo = list[0];
        considerFirst = false;
    } 
    _.each(list, function(element, index, list){
        if (index > 0 || considerFirst) {
            memo = iteratee(memo, element, index, list);
        }
    });
    return memo;
};

这样,如果没有传递参数,你只设置memo

请注意,您不需要用var 初始化memo。将memo 作为参数进行您需要的所有初始化。

还请注意,我删除了对在普通对象上使用 reduce 的支持。当您将对象传递给_.each 时,index 参数的值不是数字索引,而是该条目的键,它可能是也可能不是整数。这与我们的index &gt; 0 不匹配,检查我们是否正在查看第一个条目。有很多方法可以解决这个问题,但这似乎不是您问题的核心。如果您想了解如何使其工作,请查看实际的 underscore implementation

更新:SpiderPig 建议的实现不依赖于index,因此可以处理对象,而不仅仅是数组。

最后,值得指出的是 underscore's implementation_.reduce 使用 for 循环而不是 _.each

【讨论】:

  • 感谢您的详尽解释。对此,我真的非常感激。我现在明白我应该如何将memo 的支票放在_.each() 之外。谢谢你。但是当我尝试你的代码时,它不能在没有提供备忘录的情况下将 Object 作为列表工作。我想这是因为你用list[0] 分配备忘录。似乎 SpiderPig 将其设置为 memo = elem 并且它正在工作。但真的,如果我不想依赖_.each,那么支持我的reduce() 将数组和对象作为列表会变得更加困难吗?
【解决方案2】:

这是我能想到的最短版本。

_.reduce = function(list, iteratee, memo){
  var memoUndefined = arguments.length < 3;
  _.each(list, function(elem, index, list){
    if(memoUndefined) {
      memoUndefined = false;
      memo = elem;
    } else memo = iteratee(memo, elem, index, list);
  });
  return memo;
};

【讨论】:

  • 谢谢。它适用于数组和对象作为列表,也适用于有/没有memo。我猜elem 即使在列表是 Ojbect 的情况下也会保持其索引顺序?这是正确的吗?
  • 似乎如果不使用_.each(),实现reduce() 会变得更加复杂,它同时支持Array 和Object 作为其列表。
【解决方案3】:

Reduce 接受三个参数:集合(数组或对象)、回调和累加器(这是可选的)。

Reduce 遍历集合,调用回调并跟踪累加器中的结果。

如果没有传入累加器,我们会将其设置为集合的第一个元素。

如果累加器可用,我们会将累加器设置为等于调用回调并传入当前累加器和集合的当前元素的结果。请记住:Javascript 以从右到左的顺序执行其操作,这意味着运算符的右侧首先出现,然后才分配给左侧的变量。

  _.reduce = function(collection, callback, accumulator){
    _.each(collection, function(elem){
      return accumulator === undefined ? accumulator = collection[0] : accumulator = callback(accumulator, elem);
    });

    return accumulator;
  };

【讨论】:

  • 所以如果callback 在某处返回undefined,你会在下一次迭代中再次传递第一个元素吗?
  • 我看到您如何通过递归调用回调返回三元运算符。这是否意味着它会不断将结果从_.each() 累积到参数对象的accumulator,直到列表的所有迭代都完成?另外,我检查了您的代码,它似乎适用于 1) Array 作为带有备忘录的列表,2) Array 作为没有备忘录的列表,3) Object 作为带有备忘录的列表,但不适用于 Object 作为没有备忘录的列表.它返回undefined。我将您的功能保存到var reducereduce({"one": 1, "two": 2, "three": 3}, function(a, b){ return a + b; }); // returns undefined
  • 即使您的收藏是对象,您也可以通过collection[0] 访问您的收藏吗?
  • 好的,当我将 return 语句中的 accumulator = collection[0] 更改为 accumulator = elem 时,它正在工作。
  • @RichardKho 我试着玩一些测试用例,我注意到如果你没有在你的 sn-p 的第三行包含return,它仍然可以工作。这是为什么呢?
猜你喜欢
  • 2014-08-09
  • 2016-04-04
  • 1970-01-01
  • 1970-01-01
  • 2014-01-22
  • 2019-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多