如何重新创建Underscore.js _.reduce方法?

时间:2015-05-16 15:40:22

标签: javascript functional-programming underscore.js reduce

出于教育目的,我试图重新创建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参数list表示将返回的最终值。因为不这样做会重复计算第一个元素/属性。在创建具有函数式编程风格的函数时,我无法想到如何使用_.each()forEach()来执行此类操作。

这是reduce()的部分功能。它在提供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;
};

3 个答案:

答案 0 :(得分:4)

这是我能提出的最短版本。

_.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;
};

答案 1 :(得分: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;
  };

答案 2 :(得分:2)

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

以您的代码为出发点:

reduce
但是,这仍然是不对的。我们还需要更新您的违约方式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初始化memo。将var作为参数可以完成所需的所有初始化。

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

更新:SpiderPig建议的实现并不依赖index > 0,因此可以使用对象,而不仅仅是数组。

最后,值得指出index _.reduce使用for循环而非_.each