问题
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