Відкрийте для себе можливості функціонального програмування з масивами JavaScript. Навчіться ефективно трансформувати, фільтрувати та зводити дані за допомогою вбудованих методів.
Опановуємо функціональне програмування з масивами JavaScript
У постійно мінливому ландшафті веб-розробки JavaScript продовжує залишатися наріжним каменем. У той час як об’єктно-орієнтовані та імперативні парадигми програмування вже давно домінують, функціональне програмування (ФП) набирає значної популярності. ФП наголошує на незмінності, чистих функціях і декларативному коді, що веде до більш надійних, зручних в обслуговуванні та передбачуваних додатків. Один із найпотужніших способів застосувати функціональне програмування в JavaScript — це використовувати його власні методи масивів.
Цей вичерпний посібник розповість про те, як ви можете використати потужність принципів функціонального програмування за допомогою масивів JavaScript. Ми дослідимо ключові концепції та продемонструємо, як застосовувати їх за допомогою таких методів, як map
, filter
і reduce
, трансформуючи спосіб обробки маніпулювання даними.
Що таке функціональне програмування?
Перш ніж заглиблюватися в масиви JavaScript, давайте коротко визначимо функціональне програмування. По суті, ФП — це парадигма програмування, яка розглядає обчислення як оцінку математичних функцій і уникає зміни стану та змінних даних. Ключові принципи включають:
- Чисті функції: Чиста функція завжди дає однаковий результат для однакового вхідного значення та не має побічних ефектів (не змінює зовнішній стан).
- Незмінність: Дані, після створення, не можуть бути змінені. Замість зміни існуючих даних створюються нові дані з потрібними змінами.
- Функції першого класу: З функціями можна поводитися як з будь-якою іншою змінною – їх можна присвоювати змінним, передавати як аргументи іншим функціям і повертати з функцій.
- Декларативний проти імперативного: Функціональне програмування тяжіє до декларативного стилю, де ви описуєте *що* ви хочете досягти, а не до імперативного стилю, який детально описує *як* цього досягти крок за кроком.
Прийняття цих принципів може призвести до коду, який легше зрозуміти, тестувати та налагоджувати, особливо в складних додатках. Методи масивів JavaScript ідеально підходять для реалізації цих концепцій.
Сила методів масивів JavaScript
Масиви JavaScript оснащені багатим набором вбудованих методів, які дозволяють складне маніпулювання даними без використання традиційних циклів (наприклад, for
або while
). Ці методи часто повертають нові масиви, сприяючи незмінності, і приймають функції зворотного виклику, забезпечуючи функціональний підхід.
Давайте розглянемо найфундаментальніші функціональні методи масивів:
1. Array.prototype.map()
Метод map()
створює новий масив, заповнений результатами виклику наданої функції для кожного елемента у викликаючому масиві. Він ідеально підходить для перетворення кожного елемента масиву на щось нове.
Синтаксис:
array.map(callback(currentValue[, index[, array]])[, thisArg])
callback
: Функція, яку потрібно виконати для кожного елемента.currentValue
: Поточний елемент, який обробляється в масиві.index
(необов’язково): Індекс поточного елемента, який обробляється.array
(необов’язково): Масив, для якого було викликаноmap
.thisArg
(необов’язково): Значення, яке використовувати якthis
під час виконанняcallback
.
Основні характеристики:
- Повертає новий масив.
- Оригінальний масив залишається незмінним (незмінність).
- Новий масив матиме таку саму довжину, як і оригінальний масив.
- Функція зворотного виклику повинна повертати перетворене значення для кожного елемента.
Приклад: Подвоєння кожного числа
Уявіть, що у вас є масив чисел, і ви хочете створити новий масив, де кожне число подвоєно.
const numbers = [1, 2, 3, 4, 5];
// Використання map для перетворення
const doubledNumbers = numbers.map(number => number * 2);
console.log(numbers); // Вихід: [1, 2, 3, 4, 5] (оригінальний масив не змінюється)
console.log(doubledNumbers); // Вихід: [2, 4, 6, 8, 10]
Приклад: Витяг властивостей з об’єктів
Звичайним випадком використання є вилучення певних властивостей з масиву об’єктів. Припустимо, у нас є список користувачів і ми хочемо отримати лише їхні імена.
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
const userNames = users.map(user => user.name);
console.log(userNames); // Вихід: ['Alice', 'Bob', 'Charlie']
2. Array.prototype.filter()
Метод filter()
створює новий масив з усіма елементами, які проходять перевірку, реалізовану наданою функцією. Він використовується для вибору елементів на основі умови.
Синтаксис:
array.filter(callback(element[, index[, array]])[, thisArg])
callback
: Функція, яку потрібно виконати для кожного елемента. Вона повинна повернутиtrue
, щоб залишити елемент, абоfalse
, щоб відхилити його.element
: Поточний елемент, який обробляється в масиві.index
(необов’язково): Індекс поточного елемента.array
(необов’язково): Масив, для якого було викликаноfilter
.thisArg
(необов’язково): Значення, яке використовувати якthis
під час виконанняcallback
.
Основні характеристики:
- Повертає новий масив.
- Оригінальний масив залишається незмінним (незмінність).
- Новий масив може містити менше елементів, ніж оригінальний масив.
- Функція зворотного виклику повинна повертати логічне значення.
Приклад: Фільтрування парних чисел
Давайте відфільтруємо масив чисел, щоб залишити лише парні числа.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Використання filter для вибору парних чисел
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(numbers); // Вихід: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(evenNumbers); // Вихід: [2, 4, 6, 8, 10]
Приклад: Фільтрування активних користувачів
З нашого масиву користувачів давайте відфільтруємо користувачів, які позначені як активні.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: false }
];
const activeUsers = users.filter(user => user.isActive);
console.log(activeUsers);
/* Вихід:
[
{ id: 1, name: 'Alice', isActive: true },
{ id: 3, name: 'Charlie', isActive: true }
]
*/
3. Array.prototype.reduce()
Метод reduce()
виконує надану користувачем функцію зворотного виклику «reducer» для кожного елемента масиву по порядку, передаючи значення, що повертається з обчислення, для попереднього елемента. Кінцевим результатом виконання reducer для всіх елементів масиву є одне значення.
Це, мабуть, найуніверсальніший з методів масиву і є наріжним каменем багатьох шаблонів функціонального програмування, дозволяючи вам «звести» масив до одного значення (наприклад, суми, добутку, кількості або навіть нового об’єкта чи масиву).
Синтаксис:
array.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
callback
: Функція, яку потрібно виконати для кожного елемента.accumulator
: Значення, отримане з попереднього виклику функції зворотного виклику. Під час першого виклику цеinitialValue
, якщо воно надано; інакше це перший елемент масиву.currentValue
: Поточний елемент, який обробляється.index
(необов’язково): Індекс поточного елемента.array
(необов’язково): Масив, для якого було викликаноreduce
.initialValue
(необов’язково): Значення, яке використовувати як перший аргумент для першого викликуcallback
. ЯкщоinitialValue
не вказано, перший елемент у масиві буде використано як початкове значенняaccumulator
, а ітерація почнеться з другого елемента.
Основні характеристики:
- Повертає одне значення (яке також може бути масивом або об’єктом).
- Оригінальний масив залишається незмінним (незмінність).
initialValue
має вирішальне значення для ясності та уникнення помилок, особливо з порожніми масивами або коли тип акумулятора відрізняється від типу елемента масиву.
Приклад: Підсумовування чисел
Давайте підсумуємо всі числа в нашому масиві.
const numbers = [1, 2, 3, 4, 5];
// Використання reduce для підсумовування чисел
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // 0 - це initialValue
console.log(sum); // Вихід: 15
Пояснення:
- Виклик 1:
accumulator
дорівнює 0,currentValue
дорівнює 1. Повертає 0 + 1 = 1. - Виклик 2:
accumulator
дорівнює 1,currentValue
дорівнює 2. Повертає 1 + 2 = 3. - Виклик 3:
accumulator
дорівнює 3,currentValue
дорівнює 3. Повертає 3 + 3 = 6. - І так далі, доки не буде обчислено остаточну суму.
Приклад: Групування об’єктів за властивістю
Ми можемо використовувати reduce
, щоб перетворити масив об’єктів на об’єкт, де значення згруповані за певною властивістю. Давайте згрупуємо наших користувачів за їхнім статусом isActive
.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: false }
];
const groupedUsers = users.reduce((acc, user) => {
const status = user.isActive ? 'active' : 'inactive';
if (!acc[status]) {
acc[status] = [];
}
acc[status].push(user);
return acc;
}, {}); // Порожній об’єкт {} є initialValue
console.log(groupedUsers);
/* Вихід:
{
active: [
{ id: 1, name: 'Alice', isActive: true },
{ id: 3, name: 'Charlie', isActive: true }
],
inactive: [
{ id: 2, name: 'Bob', isActive: false },
{ id: 4, name: 'David', isActive: false }
]
}
*/
Приклад: Підрахунок випадків
Давайте порахуємо частоту кожного фрукта в списку.
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCounts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(fruitCounts); // Вихід: { apple: 3, banana: 2, orange: 1 }
4. Array.prototype.forEach()
Хоча forEach()
не повертає новий масив і часто вважається більш імперативним, оскільки його основна мета полягає у виконанні функції для кожного елемента масиву, він все ще є фундаментальним методом, який відіграє роль у функціональних шаблонах, особливо коли побічні ефекти необхідні або коли ітерація відбувається без потреби у перетвореному виводі.
Синтаксис:
array.forEach(callback(element[, index[, array]])[, thisArg])
Основні характеристики:
- Повертає
undefined
. - Виконує надану функцію один раз для кожного елемента масиву.
- Часто використовується для побічних ефектів, таких як ведення журналу в консоль або оновлення елементів DOM.
Приклад: Ведення журналу кожного елемента
const messages = ['Hello', 'Functional', 'World'];
messages.forEach(message => console.log(message));
// Вихід:
// Hello
// Functional
// World
Примітка: Для перетворень і фільтрування map
і filter
є кращими через їхню незмінність і декларативну природу. Використовуйте forEach
, коли вам конкретно потрібно виконати дію для кожного елемента без збору результатів у нову структуру.
5. Array.prototype.find()
і Array.prototype.findIndex()
Ці методи корисні для пошуку конкретних елементів у масиві.
find()
: Повертає значення першого елемента в наданому масиві, який відповідає наданій тестовій функції. Якщо жодне значення не відповідає тестовій функції, повертаєтьсяundefined
.findIndex()
: Повертає індекс першого елемента в наданому масиві, який відповідає наданій тестовій функції. В іншому випадку він повертає -1, що вказує на те, що жоден елемент не пройшов тест.
Приклад: Пошук користувача
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
const bob = users.find(user => user.name === 'Bob');
const bobIndex = users.findIndex(user => user.name === 'Bob');
const nonExistentUser = users.find(user => user.name === 'David');
const nonExistentIndex = users.findIndex(user => user.name === 'David');
console.log(bob); // Вихід: { id: 2, name: 'Bob' }
console.log(bobIndex); // Вихід: 1
console.log(nonExistentUser); // Вихід: undefined
console.log(nonExistentIndex); // Вихід: -1
6. Array.prototype.some()
і Array.prototype.every()
Ці методи перевіряють, чи всі елементи в масиві проходять перевірку, реалізовану наданою функцією.
some()
: Перевіряє, чи принаймні один елемент у масиві проходить перевірку, реалізовану наданою функцією. Він повертає логічне значення.every()
: Перевіряє, чи всі елементи в масиві проходять перевірку, реалізовану наданою функцією. Він повертає логічне значення.
Приклад: Перевірка статусу користувача
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true }
];
const hasInactiveUser = users.some(user => !user.isActive);
const allAreActive = users.every(user => user.isActive);
console.log(hasInactiveUser); // Вихід: true (тому що Bob неактивний)
console.log(allAreActive); // Вихід: false (тому що Bob неактивний)
const allUsersActive = users.filter(user => user.isActive).length === users.length;
console.log(allUsersActive); // Вихід: false
// Альтернатива, використовуючи every безпосередньо
const allUsersActiveDirect = users.every(user => user.isActive);
console.log(allUsersActiveDirect); // Вихід: false
Зв’язування методів масивів для складних операцій
Справжня сила функціонального програмування з масивами JavaScript проявляється, коли ви зв’язуєте ці методи разом. Оскільки більшість із цих методів повертають нові масиви (за винятком forEach
), ви можете плавно передавати вихід одного методу у вхід іншого, створюючи елегантні та читабельні конвеєри даних.
Приклад: Пошук імен активних користувачів і подвоєння їхніх ідентифікаторів
Давайте знайдемо всіх активних користувачів, витягнемо їхні імена, а потім створимо новий масив, де кожному імені передує число, що представляє його індекс у *відфільтрованому* списку, а їхні ідентифікатори подвоюються.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: true },
{ id: 5, name: 'Eve', isActive: false }
];
const processedActiveUsers = users
.filter(user => user.isActive) // Отримати лише активних користувачів
.map((user, index) => ({ // Перетворити кожного активного користувача
name: `${index + 1}. ${user.name}`,
doubledId: user.id * 2
}));
console.log(processedActiveUsers);
/* Вихід:
[
{ name: '1. Alice', doubledId: 2 },
{ name: '2. Charlie', doubledId: 6 },
{ name: '3. David', doubledId: 8 }
]
*/
Цей зв’язаний підхід є декларативним: ми вказуємо кроки (фільтрування, потім відображення) без явного керування циклом. Він також є незмінним, оскільки кожен крок створює новий масив або об’єкт, залишаючи оригінальний масив users
недоторканим.
Незмінність на практиці
Функціональне програмування значною мірою покладається на незмінність. Це означає, що замість зміни існуючих структур даних ви створюєте нові з потрібними змінами. Методи масивів JavaScript, такі як map
, filter
і slice
, за своєю суттю підтримують це, повертаючи нові масиви.
Чому важлива незмінність?
- Передбачуваність: Код стає легше зрозуміти, оскільки вам не потрібно відстежувати зміни спільного змінного стану.
- Налагодження: Коли виникають помилки, легше визначити джерело проблеми, коли дані не змінюються несподівано.
- Продуктивність: У певних контекстах (наприклад, з бібліотеками керування станом, такими як Redux, або в React) незмінність забезпечує ефективне виявлення змін.
- Паралелізм: Незмінні структури даних за своєю суттю є потокобезпечними, що спрощує паралельне програмування.
Коли вам потрібно виконати операцію, яка традиційно змінює масив (наприклад, додавання або видалення елемента), ви можете досягти незмінності за допомогою таких методів, як slice
, синтаксис розгортання (...
) або поєднання інших функціональних методів.
Приклад: Додавання елемента незмінно
const originalArray = [1, 2, 3];
// Імперативний спосіб (змінює originalArray)
// originalArray.push(4);
// Функціональний спосіб за допомогою синтаксису розгортання
const newArrayWithPush = [...originalArray, 4];
console.log(originalArray); // Вихід: [1, 2, 3]
console.log(newArrayWithPush); // Вихід: [1, 2, 3, 4]
// Функціональний спосіб за допомогою slice і конкатенації (менш поширений зараз)
const newArrayWithSlice = originalArray.slice(0, originalArray.length).concat(4);
console.log(newArrayWithSlice); // Вихід: [1, 2, 3, 4]
Приклад: Видалення елемента незмінно
const originalArray = [1, 2, 3, 4, 5];
// Видалити елемент за індексом 2 (значення 3)
// Функціональний спосіб за допомогою slice і синтаксису розгортання
const newArrayAfterSplice = [
...originalArray.slice(0, 2),
...originalArray.slice(3)
];
console.log(originalArray); // Вихід: [1, 2, 3, 4, 5]
console.log(newArrayAfterSplice); // Вихід: [1, 2, 4, 5]
// Використання filter для видалення певного значення
const newValueToRemove = 3;
const arrayWithoutValue = originalArray.filter(item => item !== newValueToRemove);
console.log(arrayWithoutValue); // Вихід: [1, 2, 4, 5]
Найкращі практики та передові методи
Оскільки ви стаєте більш комфортним з функціональними методами масивів, розгляньте ці практики:
- Читабельність перш за все: Хоча зв’язування є потужним, надмірно довгі ланцюжки можуть стати важкими для читання. Подумайте про розбиття складних операцій на менші іменовані функції або використання проміжних змінних.
- Зрозумійте гнучкість
reduce
: Пам’ятайте, щоreduce
може створювати масиви або об’єкти, а не лише окремі значення. Це робить його неймовірно універсальним для складних перетворень. - Уникайте побічних ефектів у зворотних викликах: Прагніть зберігати ваші зворотні виклики
map
,filter
іreduce
чистими. Якщо вам потрібно виконати дію з побічними ефектами,forEach
часто є більш відповідним вибором. - Використовуйте стрілочні функції: Стрілочні функції (
=>
) надають стислий синтаксис для функцій зворотного виклику та по-різному обробляють прив’язку `this`, що часто робить їх ідеальними для функціональних методів масивів. - Розгляньте бібліотеки: Для більш просунутих шаблонів функціонального програмування або якщо ви інтенсивно працюєте з незмінністю, бібліотеки, такі як Lodash/fp, Ramda або Immutable.js, можуть бути корисними, хоча вони не є суворо необхідними для початку роботи з функціональними операціями з масивами в сучасному JavaScript.
Приклад: Функціональний підхід до агрегування даних
Уявіть, що у вас є дані про продажі з різних регіонів і ви хочете обчислити загальний обсяг продажів для кожного регіону, а потім знайти регіон з найвищими продажами.
const salesData = [
{ region: 'North', amount: 100 },
{ region: 'South', amount: 150 },
{ region: 'North', amount: 120 },
{ region: 'East', amount: 200 },
{ region: 'South', amount: 180 },
{ region: 'North', amount: 90 }
];
// 1. Обчисліть загальний обсяг продажів за регіоном за допомогою reduce
const salesByRegion = salesData.reduce((acc, sale) => {
acc[sale.region] = (acc[sale.region] || 0) + sale.amount;
return acc;
}, {});
// salesByRegion буде: { North: 310, South: 330, East: 200 }
// 2. Перетворіть агрегований об’єкт на масив об’єктів для подальшої обробки
const salesArray = Object.keys(salesByRegion).map(region => ({
region: region,
totalAmount: salesByRegion[region]
}));
// salesArray буде: [
// { region: 'North', totalAmount: 310 },
// { region: 'South', totalAmount: 330 },
// { region: 'East', totalAmount: 200 }
// ]
// 3. Знайдіть регіон з найвищими продажами за допомогою reduce
const highestSalesRegion = salesArray.reduce((max, current) => {
return current.totalAmount > max.totalAmount ? current : max;
}, { region: '', totalAmount: -Infinity }); // Ініціалізуйте дуже малим числом
console.log('Sales by Region:', salesByRegion);
console.log('Sales Array:', salesArray);
console.log('Region with Highest Sales:', highestSalesRegion);
/*
Вихід:
Sales by Region: { North: 310, South: 330, East: 200 }
Sales Array: [
{ region: 'North', totalAmount: 310 },
{ region: 'South', totalAmount: 330 },
{ region: 'East', totalAmount: 200 }
]
Region with Highest Sales: { region: 'South', totalAmount: 330 }
*/
Висновок
Функціональне програмування з масивами JavaScript — це не просто стилістичний вибір; це потужний спосіб писати чистіший, передбачуваніший і надійніший код. Застосовуючи такі методи, як map
, filter
і reduce
, ви можете ефективно перетворювати, запитувати та агрегувати свої дані, дотримуючись основних принципів функціонального програмування, особливо незмінності та чистих функцій.
Продовжуючи свій шлях у розробці JavaScript, інтеграція цих функціональних шаблонів у ваш щоденний робочий процес, безсумнівно, призведе до більш зручних в обслуговуванні та масштабованих програм. Почніть з експериментів з цими методами масивів у своїх проектах, і ви незабаром відкриєте їхню величезну цінність.