Skip to main content

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

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

При опросе кандидатов, я обычно спрашивал их о нескольких методах 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, 0);

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

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

foldl (+) 0 [1, 2, 3]

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

Две задачи

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

const scoreArray = [
  { 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.

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

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

function maxByScore(a, b) {
  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 функции, которую мы только что определили.

function reduce(array, rule, initial) {
  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
];

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

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

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

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

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

const nestedArray = [
  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 также могут быть легко реализованы. Мы можем сделать это на практике, независимо от некоторых проблем производительности.

function map(L, fn) {
  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, когда это необходимо. Потому что это позволяет мне сосредоточиться на самой важной части, минимизируя код.

Image
sa code