Published on
Updated on 

Уменьшение в JavaScript

Authors
  • Эта публикация - перевод статьи. Ее автор - Yuan Chuan. Оригинал доступен по ссылке ниже:

    Reduce in JavaScript

При опросе кандидатов, я обычно спрашивал их о нескольких методах Array в JavaScript, которые они использовали, и позволял им реализовать один из методов на бумаге, например, some или map. Если кто-то знает, что функции можно передавать, это не должно быть трудной задачей.

Функция reduce упоминается реже всего.

Обсуждение

На прошлой неделе Софи Алперт поделилась в Твиттере своим правилом для функции reduce. Затем состоялась горячая дискуссия, так как последние два правила в списке выглядели противоречиво для участников этого обсуждения.

Я и сам часто использую эту функцию – не только для суммирования и умножения чисел, но и для составления нового массива или объекта. Правило Софи вынудило меня снова обратиться к этой функции.

Reduce vs Fold

Reduce – функция высшего порядка, обычно она вызывается через fold в функциональных языках программирования. Для меня fold – это определенно лучшее имя по сравнению с reduce.

reduce пересекает список слева направо, reduceRight делает с другого направления. Вы, наверное, видели foldl и foldr где-то еще, они делают то же самое, но при этом выглядят читабельно.

В JavaScript, если в reduce функцию не передано начальное значение, будет использован первый элемент в данном массиве.

    
    // Первый элемент используется как начальное значение
    // когда второй элемент отсутствует.
    [1, 2, 3].reduce((a, b) => a + b);

Вот почему я предпочитаю foldl1 или foldr1 в других языках, таких как Haskell, который явно использует первый или последний элемент в качестве начального значения.

    
    -- Интуитивней
    foldl1 (+) [1, 2, 3]

Две задачи

Есть две простые задачи с учетом следующих данных и решений с обычными циклами:

      { name: 'Jim', score: 99 },
      { name: 'Han', score: 55 },
      { name: 'Tom', score: 87 },
      { name: 'Ana', score: 50 }
    ];

а) Получаем общий балл всех предметов

    function getScoreSum(array) {
      let ret = 0;
      array.forEach(item => {
        ret += item.score;
      });
      return ret;
    }

б) Получаем предмет с наибольшим количеством очков

    function getHighest(array) {
      let ret = {};
      array.forEach(item => {
        if (!ret.score || (ret.score < item.score)) {
          ret = item;
        }
      });
      return ret;
    }

Просмотр шаблонов

Вышеуказанные две функции имеют нечто общее:

  1. Начальное значение.
  2. Правило о том, как обновить начальное значение для каждого элемента в списке.
  3. Возврат последнего обновленного значения.

Мы можем определить функцию для абстракции повторяющегося шаблона. На самом деле, это упрощенная версия Array.prototype.reduce.

    function reduce(array, rule, initial) {
      // initialization
      let result = initial;
      // traverse through the list and update the result
      // according to the custom rule
      array.forEach(item => {
        result = rule(result, item);
      });
      // return the final result
      return result;
    }

Теперь перепишем решения, чтобы увидеть, как они будут выглядеть в reduce.

      return reduce(array, (ret, item) =>
        ret + item.score, 0
      );
    }
    function getHighest(array) {
      return reduce(array, (ret, item) =>
        ret.score > item.score ? ret : item, {}
      );
    }

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

      return a.score > b.score ? a : b;
    }
    function getHighest(array) {
      return reduce(array, maxByScore, {});
    }

Различия

Выше, getScoreSum()а getHighest() также есть другое место.

  1. getScoreSum() возвращает номер.
  2. getHighest() возвращает объект, тип совпадает с элементами в списке.

Обычно он называется асимметричным в своих типах, когда тип возвращаемого значения отличается от типа элементов в списке. В противном случае это симметрично .

Первые два правила в твиттере Софи в основном относились к симметричным функциям.

    .reduce((a, b) => a + b, 0);
    .reduce((a, b) => a * b, 1);

Иногда удобно сделать первый элемент в качестве начального значения, когда он симметричен . Также мы можем сократить одну итерацию в цикле. Итак, здесь есть небольшие модификации reduce функции, которую мы только что определили.

      let hasInitial = arguments.length > 2;
    
      // make the first element as initial value
      // when the initial argument is missing
      let result = hasInitial ? initial : array[0];
      let start = hasInitial ? 0 : 1;
    
      for (let i = start; i < array.length; ++i) {
        result = rule(result, array[i]);
      }
      // return the final result
      return result;
    }

Теперь намного проще:

    function getHighest(array) {
      return reduce(array, (a, b) => a.score > b.score ? a : b);
    }

Составление списка

Хорошим примером использования списков reduce является реализация функции flat или flatMap.

    const nestedArray = [
      1, 2, 3, [4, 5], 6
    ];

С нормальным циклом:

      let ret = [];
      array.forEach(n => {
        ret = ret.concat(n);
      });
      return ret;
    }

Опять же, мы видим образец разоблачения. Итак, давайте попробуем сделать это с reduce.

      return array.reduce((ret, n) => ret.concat(n), []);
    }

Как видите, все не так плохо. Мы можем пойти дальше, чтобы поддержать выравнивание на массиве с глубоким вложением:

      1, 2, 3, [4, 5],
      [[6, 7], 8], 9, 10
    ];
    
    function flat(array) {
      return array.reduce((ret, n) =>
        ret.concat(Array.isArray(n) ? flat(n) : n), []
      );
    }

Другие примеры

Некоторые другие функции reduce также могут быть легко реализованы. Мы можем сделать это на практике, независимо от некоторых проблем производительности.

      return L.reduce((acc, n, i) => [...acc, fn(n, i, L)], []);
    }
    
    function reverse(L) {
      return L.reduce((acc, n) => [n, ...acc], []);
    }
    
    function join(L, sep='') {
      return L.reduce((a, b) => a + sep + b);
    }
    
    function length(L) {
      return L.reduce(acc => acc + 1, 0);
    }

Мысли

Абстракции - это способы мышления.

Если производительность не имеет решающего значения, я все равно предпочитаю использовать функцию reduce, когда это необходимо. Потому что это позволяет мне сосредоточиться на самой важной части, минимизируя код.