Две парадигмы
Когда мы говорим об определенном языке программирования или стиле программирования как о «функциональном» или «императивном», мы имеем в виду парадигмы языка программирования. Эти парадигмы являются классификациями языков программирования, и обе эти парадигмы существуют в Javascript.
Парадигма функционального программирования избегает мутации данных, вместо этого используя «чистые» функции, которые всегда выдают один и тот же результат при одних и тех же входных данных и не производят никаких побочных эффектов. Функциональное программирование также подпадает под более широкую классификацию декларативных языков программирования — языков, которые состоят из выражений, а не из команд.
Императивное программирование состоит из серии команд, которые изменяют состояние системы. Часто говорят, что императивное программирование фокусируется на описании того, как работает программа.
Функциональные методы в JavaScript, на которых мы сосредоточимся, — это операции со списками: map
, filter
и reduce
. Логика для них определяется передачей функции в качестве параметра, известной как лямбда-функция.
Аргументы за и против
Говорят, что функциональный стиль намного выразительнее. Функциональный стиль также, как правило, менее подвержен ошибкам из-за принципа неизменности и отсутствия побочных эффектов, что облегчает анализ и обдумывание программы. Однако с императивным стилем у вас есть гораздо более тонкий контроль над тем, что делает ваша программа, и производительность обычно выше. Надеюсь, после прочтения этого вы будете лучше подготовлены, чтобы составить собственное мнение.
Сравнивать подобное с подобным
map
В JavaScript функция map
создает новый массив с результатами выполнения лямбда-функции для каждого элемента массива. Например, мы можем удвоить каждый элемент в массиве следующим образом:
const array = [1, 2, 3];
const newArray = array.map(number => number * 2);
console.log(newArray);
// [2, 4, 6]
Одним из свойств функции map
является то, что выходной массив всегда имеет ту же длину, что и введенный массив. Поэтому, если у вас есть операция, в которой выходной массив может иметь длину, отличную от введенного массива, вам следует рассмотреть альтернативный метод.
Если бы мы хотели выполнить эту операцию в императивном стиле, мы бы создали экземпляр массива таким же образом, но затем переназначили бы каждому элементу значение, которое само по себе является двойным. В коде это будет выглядеть так:
const array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
array[i] = array[i] * 2;
}
console.log(array);
// [2, 4, 6]
filter
Функция filter
сравнивает каждый элемент с функцией оценки, и если функция оценки возвращает true для этого элемента, он остается в массиве, если нет, то отбрасывается. Например:
const array = [1, 2, 3];
const newArray = array.filter(number => number < 3);
console.log(newArray);
// [1, 2]
Эквивалентная операция в императивном стиле будет следующей, обратите внимание на перевернутое условие:
const array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
if (array[i] > 3) {
array.splice(i, 1);
}
}
console.log(array);
// [1, 2]
Мы используем здесь splice
, потому что ключевое слово delete
не закрывает создаваемый им пробел, вместо этого оставляя значение undefined
и сохраняя длину массива прежней. Первый аргумент для splice — это начальный индекс, а второй — количество удаляемых элементов. Как и другие императивные методы, splice
изменяет массив на месте.
reduce
Метод reduce
является самым мощным в наборе операций со списками, доступных в настоящее время. Как и раньше, мы передаем лямбда-функцию, однако теперь эта лямбда-функция способна влиять на ранее возвращенные значения, поскольку у нас есть доступ к полному возвращаемому значению, которое в настоящее время находится на каждой итерации. Кроме того, мы вообще не обязаны возвращать массив, мы можем эффективно возвращать любой тип, даже объекты.
В этом конкретном случае лямбда-функция известна как редуктор, и по мере продвижения по массиву мы постепенно модифицируем и создаем «аккумулятор», одно выходное значение. На этот раз сигнатура лямбда-функции немного отличается: первым элементом является accumulator
, а вторым элементом является текущий элемент, который мы рассматриваем. Кроме того, сам reduce
принимает не только функцию редуктора, но и необязательное начальное значение. Это важно в определенных сценариях, когда ваши операции могут привести к нежелательному результату из-за приведения типов.
У нас есть следующий пример, где мы хотим суммировать весь массив:
const array = [1, 2, 3, 4, 5];
const sum = array.reduce((accumulator, next) => accumulator + next);
console.log(sum);
// 15
Это может немного сбивать с толку, поэтому давайте немного углубимся.
При первом выполнении, поскольку мы не указали начальное значение, начальное значение accumulator
по умолчанию равно первому значению в массиве, то есть 1
. next
— следующий элемент массива, поэтому 2
. Теперь мы заявляем, что хотим, чтобы следующее значение accumulator
— возвращаемое функцией значение — было результатом accumulator + next
, что, конечно же, равно 3. Этот процесс продолжается до тех пор, пока массив не будет исчерпан.
╔══════════════════╦═════════════╦══════╦══════════════╗ ║ Iteration ║ Accumulator ║ Next ║ Return value ║ ╠══════════════════╬═════════════╬══════╬══════════════╣ ║ First iteration ║ 1 ║ 2 ║ 3 ║ ║ Second iteration ║ 3 ║ 3 ║ 6 ║ ║ Third iteration ║ 6 ║ 4 ║ 10 ║ ║ Fourth iteration ║ 10 ║ 5 ║ 15 ║ ╚══════════════════╩═════════════╩══════╩══════════════╝
В отличие от map
, где введенный массив всегда имеет ту же длину, что и выходной массив, если ваш выходной массив может иметь другую длину или фактически другой тип по сравнению с введенным массивом, вам, скорее всего, понадобится reduce
.
Эквивалентная программа в императивном стиле будет выглядеть следующим образом:
let sum = 0;
const array = [1, 2, 3, 4, 5];
for (let i = 0; i < array.length; i++) {
sum += array[i];
}
console.log(sum);
// 15
Здесь мы заполняем значение счетчика, увеличивая его на значение, содержащееся в массиве.
Из-за того, как типы оцениваются в JavaScript, мы можем внести небольшое изменение, чтобы получить принципиально другой результат. Инфиксный оператор +
перегружен в JavaScript, что означает, что он может выполнять более одной операции в зависимости от аргументов. Предыдущий пример передал тип Number
в левой части операции +
, поэтому он оценивался как сложение. Однако если мы изменим начальное значение на ''
(пустая строка), так что левая часть теперь имеет тип String
, операция оценивается как конкатенация строк, при этом значение next
преобразуется в String
на каждом этапе. Таким образом, у нас есть код:
const array = [1, 2, 3, 4, 5];
const sum = array.reduce((accumulator, next) => accumulator + next, '');
// '12345'
Итак, серия итераций выглядит следующим образом:
╔══════════════════╦═════════════╦══════╦═══════════════╗ ║ Iteration ║ Accumulator ║ Next ║ Return value ║ ╠══════════════════╬═════════════╬══════╬═══════════════╣ ║ First iteration ║ '' ║ 1 ║ '1' ║ ║ Second iteration ║ '1' ║ 2 ║ '12' ║ ║ Third iteration ║ '12' ║ 3 ║ '123' ║ ║ Fourth iteration ║ '123' ║ 4 ║ '1234' ║ ║ Fifth iteration ║ '1234' ║ 5 ║ '12345' ║ ╚══════════════════╩═════════════╩══════╩═══════════════╝
Мой любимый способ использования сокращения — использовать его для создания объектов. Текущее предложение в JavaScript для обработки объектов как итерируемых выглядит тусклым, и reduce
, возможно, является единственным прилично выразительным способом перехода от одного типа к другому без явного принуждения или промежуточных шагов. К сожалению, он работает только в одном направлении, от Array
до Object
. Несмотря на это, он обычно позволяет создавать очень элегантные решения. Например, возьмем задачу подсчета количества раз, когда определенная строка встречается в массиве.
const array = ['a', 'b', 'a', 'c', 'b', 'a'];
const tally = array.reduce((accumulator, next) => ({
...accumulator,
[next]: accumulator[next] + 1 || 1
}), {});
console.log(tally);
// { a: 3, b: 2, c: 1 }
Потребовалось всего одно выражение, чтобы перейти от списка строк к принципиально другому представлению этих данных в виде объекта.
Немного поясню: чтобы получить возвращаемое значение лямбда-функции, мы распространяем текущий аккумулятор на возвращаемое значение, чтобы не потерять предыдущие данные, а затем указываем, что свойство со значением next
, т.е. 'a'
, 'b'
или 'c'
— следует либо увеличить, если он уже существует, либо создать экземпляр со значением 1.
Хорошее время для использования for
break
ing
Цикл for
предлагает нам функциональность break
, чего нет у методов массива. Это означает, что если у нас есть операции, из которых мы могли бы выйти раньше, мы должны рассмотреть возможность использования цикла for
.
Скажем, ради аргумента, вы не хотите, чтобы какой-либо элемент был больше 100, и вы знали, что ваш массив отсортирован в порядке возрастания, вы могли бы break
— выйти из цикла — на итерации, где вы нарушите это условие.
const array = [1, 2, 3, 49, 51, 53];
for (let i = 0; i < array.length; i++) {
if (array[i] > 100 / 2) {
break;
}
array[i] = array[i] * 2;
}
console.log(array);
// [2, 4, 6, 98, 51, 53]
Чанкинг
Иногда наиболее элегантным решением проблемы может быть пошаговое выполнение массива с определенным размером шага. Например, если у нас есть массив данных за многие месяцы, мы можем извлечь выгоду из размера шага 12, чтобы мы могли просматривать данные за один и тот же месяц за несколько лет. Это возможно только с циклом for
, так как функциональный метод будет проходить через массив по одному элементу за раз.
Для этого мы модифицируем третью часть определения цикла for после второй точки с запятой, чтобы переменная цикла i
каждый раз увеличивалась на 12.
const monthData = [/* ... */]
for (let i = 0; i < monthData.length; i += 12) {
/* do something */
}
Учитывая другие элементы
Более уместно использовать императивный стиль, когда нам интересно посмотреть на соседние элементы или любые другие элементы в массиве.
Например, скажем, мы анализируем список показаний и хотим иметь возможность идентифицировать внезапные всплески или падения, которые определяются как увеличение или уменьшение некоторого произвольного значения по сравнению с предыдущими значениями. Если бы мы хотели получить доступ к соседним элементам в императивном стиле, мы бы расширили использование нотации с квадратными скобками, и таким образом решение было бы единообразным и непротиворечивым. Однако, если бы мы попытались сделать то же самое в функциональном стиле, мы обнаружили бы, что нам все еще нужно реализовать нотацию с квадратными скобками, и поэтому теперь мы смешиваем два способа доступа к нашим элементам массива. Затем мы можем использовать это обозначение в квадратных скобках для третьего (или четвертого при использовании сокращения) параметра лямбда-функции, но этим мы искушаем нарушение неизменности. Или мы можем использовать его в массиве, как он определен в верхней области видимости, но при этом мы выходим за пределы аккуратного и автономного контекста лямбда-функции и нарушаем его.
Подведение итогов
Догматичность в применении того или иного стиля не приведет к лучшим результатам. Имейте в виду, что любая попытка принудительно заставить тот или иной стиль вести себя так, как вы хотите, когда он для этого не подходит, приведет к созданию кода, который будет непонятен вашим товарищам по команде, да и вам самим тоже.
Думаете, ClearScore – это интересное место для работы? Мы нанимаем: clear-score.workable.com