Функциональное программирование — один из философских подходов при разработке программного обеспечения, который захватил мир JavaScript. Несмотря на то что функциональное программирование берет начало из таких языков, как Haskell и Lisp, оно все больше ассоциируется с разработчиками JavaScript, и не зря. Как мы вскоре узнаем, JavaScript нативно реализует все функциональные концепции, о которых будет идти речь в этой статье. Возможно, вы уже используете некоторые из этих методов, даже не подозревая об этом.
Но что именно они собой представляют и как разработчики JavaScript используют их для улучшения кода? Рассмотрим 4 концепции функционального программирования, которые можно легко реализовать в JavaScript.
1. Функции высшего порядка
Это ключевое понятие в функциональном программировании, и они также чрезвычайно полезны в JavaScript. Функция высшего порядка — это функция, которая принимает одну или несколько функций в качестве аргумента или возвращает функцию в качестве результата. Это позволяет разработчикам писать более гибкий и многократно используемый код.
Отличным примером функции высшего порядка в JavaScript является метод Array.prototype.map()
. Он принимает функцию обратного вызова в качестве аргумента и применяет ее к каждому элементу массива, а затем возвращает новый массив с результатами.
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(number => number * 2);
console.log(doubledNumbers); // Вывод: [2, 4, 6, 8, 10]
В этом примере у нас есть массив чисел, и мы хотим удвоить каждое число. Вместо того чтобы перебирать массив и вручную удваивать каждое число, мы применяем метод map()
и передаем функцию обратного вызова, которая удваивает текущее число. Затем метод map()
использует функцию обратного вызова для каждого элемента массива и возвращает новый массив с удвоенными числами.
Функции высшего порядка в JavaScript также можно использовать для создания более сложной, вложенной логики. Например, метод Array.prototype.reduce()
задействуют для того, чтобы взять несколько массивов и объединить их в один массив. Метод reduce()
берет в качестве аргумента функцию обратного вызова, которая применяется к каждому элементу массива, и возвращает одно значение, которое может быть новым массивом. Рассмотрим пример:
const numbers = [1, 2, 3, 4];
const letters = ['a', 'b', 'c', 'd'];
const combined = numbers.reduce((acc, curr) => {
acc.push(curr);
acc.push(letters[curr-1]);
return acc;
}, []);
console.log(combined); // Вывод: [1, "a", 2, "b", 3, "c", 4, "d"]
Здесь у нас есть два массива: numbers
и letters
. Метод reduce()
берет массив numbers
и применяет функцию обратного вызова, которая принимает текущее число и соответствующую букву из массива letters
и помещает их оба в новый, пустой массив (acc
). Функция обратного вызова применяется к каждому элементу массива numbers
, и конечным результатом станет один объединенный массив, содержащий и буквы, и числа.
Функции обратного вызова — это функции, передающиеся в качестве аргумента другим функциям, которые будут вызваны позже. В JavaScript их также называют “функциями первого класса”, потому что их можно передавать как любую другую переменную. Они также могут вызываться или возвращаться в качестве результата другой функции. В приведенных выше примерах функции, переданные методам map()
и reduce()
, являются обратными вызовами.
Реализация функций высшего порядка кажется естественным процессом в контексте JavaScript. Дальше посмотрим, какими еще средствами располагает этот язык для поддержки функционального программирования.
2. Неизменяемость
Смысл этой концепции в том, что после создания каких-либо данных их нельзя менять. В функциональном программировании неизменяемость является основным понятием. Благодаря ей создается более предсказуемая кодовая база, которую легко поддерживать. Происходит это за счет снижения вероятности возникновения ошибок, неизбежно возникающей из-за непредвиденных побочных эффектов модификации данных.
В JavaScript часто используются объекты и массивы, которые можно изменять на месте. Например, добавляются/удаляются свойства объекта или помещаются/выгружаются элементы из массива. Однако эти действия меняют исходные данные, что может привести к непредвиденным последствиям в случае использования данных в разных местах.
Для достижения неизменяемости в JavaScript используются метод Object.assign()
(создает новый объект с нужными свойствами) и метод Array.prototype.slice()
(создает новый массив с нужными элементами). Например:
const originalNumbers = [1, 2, 3, 4, 5];
const newNumbers = originalNumbers.slice(); // создает новый массив с теми же элементами, что и originalNumbers
const originalPerson = { name: 'John', age: 30 };
const newPerson = Object.assign({}, originalPerson, { age: 31 }); // создает новый объект с теми же свойствами, что и originalPerson, но с измененным возрастом
Скорее всего, вы уже поняли, что многие встроенные методы массивов в JavaScript, такие как map()
, filter()
и reduce()
, также являются неизменяемыми. Они возвращают новый массив с преобразованными или отфильтрованными данными, а не изменяют исходный массив на месте. Следовательно, вы можете использовать эти методы для создания новых, измененных версий данных, не затрагивая при этом исходные данные. Другими словами, JavaScript максимально упрощает комбинирование функций высшего порядка с концепцией неизменяемости.
3. Замыкания
Еще одним ключевым понятием в функциональном программировании является замыкание. Замыкание — это функция, которая имеет доступ к переменным в своей лексической области видимости, даже если она вызывается за ее пределами. Такая особенность позволяет разработчикам создавать функции с приватным состоянием, которое не может быть изменено извне, что помогает достигать принципа неизменяемости кода.
Представьте себе замыкание как функцию, которая “закрывает” некоторые переменные. Таким образом, она имеет к ним доступ, даже если вызывается вне области видимости, в которой были определены переменные. Функция запоминает свое состояние, что позволяет создавать переменные, которые нельзя изменить извне.
Приведем пример. Допустим, вы хотите создать простую функцию-счетчик, которая начинается с нуля и увеличивается на единицу при каждом вызове. Для этого можно создать замыкание:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
const counter = makeCounter();
console.log(counter()); // Вывод: 0
console.log(counter()); // Вывод: 1
В этом примере функция makeCounter
создает замыкание, которое имеет приватный доступ к переменной count
и возвращает функцию, увеличивающую переменную count
на 1 при каждом вызове. Поскольку переменная count
определена внутри функции makeCounter
, она не может быть доступна или изменена извне, т.е. является приватной и защищенной.
На первый взгляд концепция замыкания обманчиво проста. Погрузимся в ее скрытые возможности. Допустим, вы хотите создать функцию, которая возвращает новую функцию, умножающую входное значение на определенное число. Для этого можно создать замыкание:
function multiplyBy(x) {
return function(n) {
return n * x;
}
}
const double = multiplyBy(2);
console.log(double(5)); // Вывод: 10
const triple = multiplyBy(3);
console.log(triple(5)); // Вывод: 15
В этом примере функция multiplyBy
создает замыкание, имеющее доступ к переменной x, и возвращает новую функцию, которая принимает одно входное значение n и умножает его на x. Возвращенная функция запоминает значение x с момента его создания и может быть использована для умножения любого числа на это конкретное значение. Это позволяет создавать несколько функций с различным поведением без необходимости их отдельного определения.
4. Композиция
Композиция — это просто процесс объединения небольших чистых функций для создания более сложных функций.
Представьте, что у вас есть функция, принимающая строку и набирающая ее заглавными буквами, и другая функция, которая принимает строку и добавляет восклицательный знак в конце. Вы можете использовать композицию для создания новой функции, которая принимает строку, пишет ее заглавными буквами, а затем добавляет в конце восклицательный знак.
function compose(f, g) {
return function(x) {
return f(g(x))
}; // простая реализация с использованием замыкания и каррирования
// (можно вносить изменения для большего кол-ва аргументов)
}
function capitalize(str) {
return str.toUpperCase();
}
function addExclamationPoint(str) {
return str + '!';
}
const shout = compose(addExclamationPoint, capitalize);
console.log(shout('hello world')); // Вывод: "HELLO WORLD!
В приведенном примере используется концепция, которую мы еще не осветили. Но, возможно, вы уже интуитивно понимаете, как работает каррирование. Это еще одна техника, используемая в функциональном программировании. Она заключается в том, что функция разбивается на ряд функций, каждая из которых принимает по одному аргументу. В нашем примере замыкание closureComposition
использует каррирование для приема двух функций f
и g
и возвращает новую функцию, которая применяет g
к своему аргументу, а затем f
к результату.
Замыкания и каррирование используются для композиции функций capitalize
и addExclamationPoint
в новую функцию высшего порядка — shout
.
Именно в этом и заключается суть композиции (и функционального программирования в целом): взять небольшие строительные блоки и создать из них нечто более мощное и выразительное.
Понимая и используя вышеперечисленные концепции, разработчики JavaScript могут создавать более эффективный и удобный в обслуживании код. А самое приятное то, что JavaScript имеет встроенную поддержку всех этих функциональных концепций.
Читайте также:
- Управление памятью JavaScript: как избежать утечек памяти и повысить производительность
- Плюсы и минусы Deno
- Масштабирование фронтенд-приложений в 2023 году
Читайте нас в Telegram, VK и Дзен
Перевод статьи Anthony Jimenez: 4 functional concepts every JavaScript developer should learn!