- Published on
- Updated on
Уменьшение в JavaScript
- Authors
Эта публикация - перевод статьи. Ее автор - 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);
Вот почему я предпочитаю 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;
}
Просмотр шаблонов
Вышеуказанные две функции имеют нечто общее:
- Начальное значение.
- Правило о том, как обновить начальное значение для каждого элемента в списке.
- Возврат последнего обновленного значения.
Мы можем определить функцию для абстракции повторяющегося шаблона. На самом деле, это упрощенная версия 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() также есть другое место.
- getScoreSum() возвращает номер.
- 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, когда это необходимо. Потому что это позволяет мне сосредоточиться на самой важной части, минимизируя код.