Как воссоздать метод 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, мне нужно впоследствии либо пропустить, либо удалить первый element или property аргумента, 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;
};

Долго искал ответы на свой вопрос в гугле. Но не смог найти. Я был бы очень признателен за ваш совет. Спасибо.

Наконец, это дополнительный код, который я придумал, который не работает, но я думаю, что он должен.

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

person KyleC    schedule 16.05.2015    source источник
comment
Возможно, вы не знаете, что старый добрый JavaScript уже поставляется с сокращением( ) для массивов, так почему бы не начать с этого, а не выполнять множество for циклов?   -  person Mike 'Pomax' Kamermans    schedule 16.05.2015
comment
Что ж, если ОП должен поддерживать IE 8, тогда у них не будет доступа к Array.prototype.reduce(). Кроме того, ОП упомянул, что это было академическое упражнение.   -  person Gent    schedule 16.05.2015
comment
Спасибо Майк за ваше мнение. Мне известен родной метод Array reduce(). Я не использовал его, потому что хотел узнать, как он составлен, плюс я хотел, чтобы он также поддерживал объект в качестве аргумента списка.   -  person KyleC    schedule 17.05.2015


Ответы (3)


Вот самая короткая версия, которую я смог придумать.

_.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;
};
person SpiderPig    schedule 16.05.2015
comment
Спасибо. Он работает как с массивом, так и с объектом в виде списка, а также с/без memo. Я предполагаю, что elem сохраняет свой индексный порядок даже в случае, когда список является Ojbect? Это верно? - person KyleC; 17.05.2015
comment
Похоже, что без использования _.each() будет намного сложнее реализовать reduce(), который поддерживает как массив, так и объект в качестве своего списка. - person KyleC; 17.05.2015

Reduce принимает три параметра: коллекцию (массив или объект), обратный вызов и аккумулятор (этот параметр необязателен).

Reduce выполняет итерацию по коллекции, которая вызывает обратный вызов и отслеживает результат в накопителе.

Если аккумулятор не передан, мы установим его в первый элемент коллекции.

Если аккумулятор доступен, мы установим аккумулятор равным результату вызова обратного вызова и передачи текущего аккумулятора и текущего элемента коллекции. Помните: Javascript выполняет свои операции в порядке справа налево, то есть правая часть оператора появляется первой, прежде чем она будет присвоена переменной слева.

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

    return accumulator;
  };
person Richard Kho    schedule 16.05.2015
comment
Итак, если callback где-то вернет undefined, вы снова передадите первый элемент в следующей итерации? - person Bergi; 16.05.2015
comment
Я вижу, как вы возвращаете тернарный оператор с рекурсивным вызовом обратного вызова. Означает ли это, что он продолжает накапливать результат от _.each() до accumulator объекта аргументов до тех пор, пока не будут завершены все итерации для списка? Кроме того, я проверил ваш код, и, похоже, он работает для 1) массива в виде списка с памяткой, 2) массива в виде списка без памятки, 3) объекта в виде списка с памяткой, но не работает для объекта как списка без памятки . Он возвращает undefined. Я сохранил вашу функцию в var reduce. reduce({"one": 1, "two": 2, "three": 3}, function(a, b){ return a + b; }); // returns undefined - person KyleC; 17.05.2015
comment
Можете ли вы получить доступ к своей коллекции с помощью collection[0], даже если ваша коллекция является объектом? - person KyleC; 17.05.2015
comment
Хорошо, это работает, когда я изменил accumulator = collection[0] в операторе возврата на accumulator = elem. - person KyleC; 17.05.2015
comment
@RichardKho Я пробовал играть с некоторыми тестовыми примерами и заметил, что если вы не включите return в третью строку своего фрагмента, он все равно будет работать точно так же. Это почему? - person TheRealFakeNews; 19.01.2016

Во-первых, вам нужен способ определить, получил ли reduce начальное значение memo, когда вы находитесь внутри функции, которую вы передаете _.each. Вы можете сделать это несколькими способами. Один из способов — просто установить флаг на основе длины arguments. Вам нужно сделать это вне вызова _.each, потому что функция, которую вы передаете _.each, будет иметь свой собственный объект arguments, маскирующий объект arguments для 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 является не числовым индексом, а ключом для этой записи, который может быть или не быть целым числом. Это не очень хорошо сочетается с нашей проверкой index > 0, чтобы увидеть, смотрим ли мы на первую запись. Есть способы обойти это, но это не кажется центральным в вашем вопросе. Ознакомьтесь с реальной реализацией подчеркивания, если хотите увидеть, как это работает.

Обновление: реализация, которую предлагает SpiderPig, не полагается на index и поэтому будет работать с объектами, а не только с массивами.

Наконец, стоит отметить, что реализация подчеркивания _.reduce использует цикл for, а не _.each.

person andyk    schedule 16.05.2015
comment
Спасибо за подробное объяснение. Я очень ценю это. Теперь я понимаю, как я должен был выставить свой чек на memo вне _.each(). Спасибо. Но когда я попробовал ваш код, он не работал с объектом в виде списка без предоставления заметки. Я думаю, это потому, что вы назначаете записку с list[0]. Кажется, SpiderPig установил его как memo = elem, и он работает. Но на самом деле, если бы я не хотел зависеть от _.each, стало бы намного сложнее поддерживать мой reduce() как для массивов, так и для объектов в виде списков? - person KyleC; 17.05.2015