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

How to re-create Underscore.js _.reduce method?

出于教育目的,我试图重新创建 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 时,我需要随后跳过或删除第一个 elementpropertylist 表示将返回的最终值。因为不这样做会重复计算第一个 element/property。我想不出在使用函数式编程风格创建函数时如何做这样的事情,涉及 _.each()forEach()

这是我的 reduce() 函数式风格,它部分起作用了。当提供 memo(初始值)时它可以正常工作,因为我不需要跳过第一个 element/property。但是当没有提供 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;
};

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

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

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

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

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

    return accumulator;
  };

首先,您需要一种方法来确定当您在传递给 _.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 > 0 检查,看看我们是否正在查看第一个条目。有很多方法可以解决这个问题,但这似乎不是您问题的核心。如果您想了解如何使其工作,请查看实际的 underscore implementation

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

最后,值得指出的是 _.reduceunderscore's implementation 使用了 for 循环而不是 _.each

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

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