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

风流意气都作罢 提交于 2019-12-10 10:49:23

问题


For education purposes, I was trying to re-create Underscore.js's _.reduce() method. While I was able to do this in an explicit style using for loops. But this is far from ideal because it mutates the original list that was supplied as an argument, which is dangerous.

I also realized that creating such method using functional programming style is harder, since it is not possible to explicitly set i value for looping.

// 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;
};

What makes me struggle is that when my reduce() is supplied with an argument, initial, I need to subsequently either skip or remove the first element or property of the argument, list for the final value that will be returned. Because not doing so will double count the first element/property. I can't think of how I could do such thing when creating the function with functional programming style, with _.each() or forEach() involved.

This is my functional style of reduce() that is working partially. It works correctly when memo(initial value) is supplied, because I don't need to skip the first element/property. But it's not working correctly when memo is not supplied, because then I'm setting memo to either first element or property, and I should be able to skip it during the looping, which I don't know how.

// 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;
};

I spent quite a long time searching for answers to my question on Google. But wasn't able to find one. I would really appreciate your advice. Thanks.

Lastly, this is an additional code I came up with which does not work, but I think it should.

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

回答1:


Here is the shortest version I could come up with.

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



回答2:


Reduce accepts three parameters: A collection (array or object), callback, and accumulator (this one is optional).

Reduce iterates through the collection, which invokes the callback and keeps track of the result in the accumulator.

If an accumulator is not passed in, we'll set it to the first element of the collection.

If an accumulator is available, we'll set the accumulator to be equal to the result of invoking the callback and passing in the current accumulator and the current element of the collection. Remember: Javascript executes its operations in right-to-left order, meaning the right side of the operator occurs first before it gets assigned to the variable on the left.

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

    return accumulator;
  };



回答3:


First, you need a way to determine whether reduce received an initial memo value when you are inside the function you pass to _.each. You could do this a number of ways. One way is to simply set a flag based on the length of arguments. You need to do this outside the _.each call because the function you pass to _.each will have its own arguments object, masking the arguments object for reduce.

Using your code as a starting point:

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

This still isn't quite right, though. We also need to update how you are defaulting memo. Currently, if memo receives a falsy value (e.g. 0), we still set it to the fist element in the list, but we don't set the flag indicating to ignore the first element. This means reduce will process the first element twice.

To get this right, you need to change how you are defaulting memo, setting it only if no argument is passed in. You could do something like this:

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

This way, you only set memo if no argument was passed.

Note that you don't need to initialize memo with var. Having memo as a parameter does all the initialization you need.

Also note that I removed support for using reduce on a plain object. When you pass an object to _.each, the value of the index parameter is not a numerical index but the key for that entry, which may or may not be an integer. This does not play well with our index > 0 check to see if we are looking at the first entry. There are ways around this, but it doesn't seem central to your question. Check out the actual underscore implementation if you want to see how to make it work.

Update: the implementation SpiderPig suggests doesn't rely on index and so would work with objects, not just arrays.

Lastly, it's worth pointing out that underscore's implementation of _.reduce uses a for loop and not _.each.



来源:https://stackoverflow.com/questions/30277593/how-to-re-create-underscore-js-reduce-method

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!