16 липня 2023 р.

Методи масивів

Масиви пропонують безліч методів. Щоб було простіше, в цьому розділі вони розбиті на групи.

Додавання/видалення елементів

Ми вже знаємо методи, які додають чи видаляють елементи з початку чи з кінця:

  • arr.push(...items) – додає елементи до кінця,
  • arr.pop() – дістає елемент з кінця,
  • arr.shift() – дістає елемент з початку,
  • arr.unshift(...items) – додає елементи в початок.

Розглянемо й інші.

splice

Як видалити елемент з масиву?

Масиви є об’єктами, тому ми можемо спробувати використати delete:

let arr = ["I", "go", "home"];

delete arr[1]; // видалимо "go"

alert( arr[1] ); // undefined

// тепер arr = ["I",  , "home"];
alert( arr.length ); // 3

Начебто, елемент був видалений, але при перевірці виявляється, що масив все ще має 3 елементи arr.length == 3.

Це нормально, тому що все, що робить delete obj.key – це видаляє значення за ключем key. Це нормально для обʼєктів, але для масивів ми звичайно хочемо, щоб інші елементи змістились і зайняли місце, що звільнилося. Ми чекаємо, що масив стане коротшим.

Тому слід застосовувати спеціальні методи.

Метод arr.splice – це універсальний «швейцарський ніж» для роботи з масивами. Вміє все: додавати, видаляти і замінювати елементи.

Його синтаксис:

arr.splice(start[, deleteCount, elem1, ..., elemN])

Він змінює arr починаючи з позиції start: видаляє deleteCount елементів і вставляє elem1, ..., elemN на їх місце. Повертається масив з видалених елементів.

Цей метод легко зрозуміти на прикладах.

Почнемо з видалення:

let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // з індексу 1 видалимо 1 елемент

alert( arr ); // ["I", "JavaScript"]

Легко, правда? Починаючи з індексу 1, він видалив 1 елемент.

У наступному прикладі ми видаляємо 3 елементи та замінюємо їх двома іншими:

let arr = ["I", "study", "JavaScript", "right", "now"];

// видалимо 3 перших елементи і замінимо їх іншими
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // отримаєм ["Let's", "dance", "right", "now"]

Тут ми бачимо, що splice повертає масив видалених елементів:

let arr = ["I", "study", "JavaScript", "right", "now"];

// видалимо 2 перших елементи
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- масив видалених елементів

Метод splice також може вставляти елементи без будь-яких видалень. Для цього нам потрібно встановити значення 0 для deleteCount:

let arr = ["I", "study", "JavaScript"];

// починаючт з індекса 2
// видалимо 0 елементів
// ваставити "complex" та "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"
Дозволяються відʼємні індекси

Тут і в інших методах масиву допускаються відʼємні індекси. Вони дозволяють почати відлік елементів з кінця, як тут:

let arr = [1, 2, 5];

// починаючи з індексу -1 (перед останнім елементом)
// видалимо 0 елементів,
// вставимо значення 3 та 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

slice

Метод arr.slice набагато простіший, ніж схожий на нього arr.splice.

Його синтаксис:

arr.slice([start], [end])

Він повертає новий масив, копіюючи до нього всі елементи від індексу start до end (не включаючи end). І start, і end можуть бути відʼємними. В такому випадку відлік буде здійснюватися з кінця масиву.

Він подібний до рядкового методу str.slice, але замість підрядків створює підмасиви.

Наприклад:

let arr = ["t", "e", "s", "t"];

alert( arr.slice(1, 3) ); // e,s (копіює з 1 до 3)

alert( arr.slice(-2) ); // s,t (копіює з -2 до кінця)

Можна викликати slice і взагалі без аргументів. arr.slice() створить копію масиву arr. Це часто використовують, щоб створити копію масиву для подальших перетворень, які не повинні змінювати вихідний масив.

concat

Метод arr.concat створює новий масив, в який копіює дані з інших масивів та додаткові значення.

Його синтаксис:

arr.concat(arg1, arg2...)

Він приймає будь-яку кількість аргументів – масивів або значень.

Результатом є новий масив, що містить елементи з arr, потімarg1, arg2 тощо.

Якщо аргумент argN є масивом, то всі його елементи копіюються. В іншому випадку буде скопійовано сам аргумент.

Наприклад:

let arr = [1, 2];

// створимо масив з: arr і [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// створимо масив з: arr, [3,4] і [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// створимо масив з: arr і [3,4], також добавимо значення 5 і 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

Зазвичай він просто копіює елементи з масивів. Інші обʼєкти, навіть якщо вони виглядають як масиви, додаються як є:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]

… Але якщо обʼєкт має спеціальну властивість Symbol.isConcatSpreadable, то він обробляється concat як масив: замість нього додаються його числові властивості. Для коректної обробки в обʼєкті повинні бути числові властивості та length:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

Перебір: forEach

Метод arr.forEach дозволяє запускати функцію для кожного елемента масиву…

Його синтаксис:

arr.forEach(function(item, index, array) {
  // ... робимо щось з item
});

Наприклад, цей код виведе на екран кожен елемент масиву:

// для кожного елементу викликається alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

А цей до того ж розповість і про свою позицію в масиві:

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} має позицію ${index} в масиві ${array}`);
});

Результат функції (якщо вона взагалі щось повертає) відкидається і ігнорується.

Пошук в масиві

Далі розглянемо методи, які допоможуть знайти що-небудь в масиві.

indexOf/lastIndexOf та includes

Методи arr.indexOf та arr.includes мають однаковий синтаксис і роблять по суті те ж саме, що і їх рядкові аналоги, але працюють з елементами замість символів:

  • arr.indexOf(item, from) – шукає item, починаючи з індексу from, і повертає індекс, на якому був знайдений шуканий елемент, в іншому випадку -1.
  • arr.includes(item, from) – шукає item, починаючи з індексу from, і повертає true, якщо пошук успішний.

Зазвичай ці методи використовуються лише з одним аргументом: item для пошуку. Типово пошук відбувається з самого початку.

Наприклад:

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

Зверніть увагу, що метод indexOf використовує суворе порівняння ===. Таким чином, якщо ми шукаємо false, він знаходить саме false, але не нуль.

Якщо ми хочемо перевірити наявність item в массиві, і нема потреби знати його точний індекс, тоді краще використати arr.includes.

Метод arr.lastIndexOf такий самий, як indexOf, але шукає справа наліво.

let fruits = ['Apple', 'Orange', 'Apple']

alert( fruits.indexOf('Apple') ); // 0 (перший Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (останній Apple)
Метод includes правильно обробляє NaN

Незначною, але вартою уваги властивістю includes є те, що він правильно обробляє NaN, на відміну від indexOf:

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (повинен бути 0, але === перевірка на рівність не працює з NaN)
alert( arr.includes(NaN) );// true (вірно)

That’s because includes was added to JavaScript much later and uses the more up to date comparison algorithm internally.

find і findIndex/findLastIndex

Уявіть, що у нас є масив обʼєктів. Як нам знайти обʼєкт за певною умовою?

Тут стане в нагоді метод arr.find(fn).

Його синтаксис такий:

let result = arr.find(function(item, index, array) {
  // якщо true - повертається поточний елемент і перебір закінчується
  // якщо всі ітерації виявилися помилковими, повертається undefined
});

Функція викликається по черзі для кожного елемента масиву:

  • item – черговий елемент масиву.
  • index – його індекс.
  • array – сам масив.

Якщо функція повертає true, пошук припиняється, повертається item. Якщо нічого не знайдено, повертається undefined.

Наприклад, у нас є масив користувачів, кожен з яких має поля id та name. Давайте знайдемо той де id == 1:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

У реальному житті масиви обʼєктів – звичайна справа, тому метод find вкрай корисний.

Зверніть увагу, що в даному прикладі ми передаємо find функцію item => item.id == 1, з одним аргументом. Це типово, інші аргументи цієї функції використовуються рідко.

Метод arr.findIndex – по суті, те ж саме, але повертає індекс, на якому був знайдений елемент, а не сам елемент, і -1, якщо нічого не знайдено.

Метод arr.findLastIndex схожий на findIndex, але шукає справа наліво, подібно до lastIndexOf.

Ось приклад:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"},
  {id: 4, name: "John"}
];

// Знайдемо індекс першого John
alert(users.findIndex(user => user.name == 'John')); // 0

// Знайдемо індекс останнього John
alert(users.findLastIndex(user => user.name == 'John')); // 3

filter

Метод find шукає один (перший) елемент, на якому функція-колбек поверне true.

На той випадок, якщо знайдених елементів може бути багато, передбачений метод arr.filter(fn).

Синтаксис цього методу схожий з find, але filter повертає масив з усіх відфільтрованих елементів:

let results = arr.filter(function(item, index, array) {
  // якщо true - елемент додається до результату, і перебір триває
  // повертається порожній масив в разі, якщо нічого не знайдено
});

Наприклад:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// повертає масив перших двох користувачів
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

Перетворення масиву

Перейдемо до методів перетворення і впорядкування масиву.

map

Метод arr.map є одним з найбільш корисних і часто використовуваних.

Він викликає функцію для кожного елемента масиву і повертає масив результатів виконання цієї функції.

Синтаксис:

let result = arr.map(function(item, index, array) {
  // повертається нове значення замість елемента
});

Наприклад, тут ми перетворюємо кожен елемент на його довжину:

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6

sort(fn)

Виклик arr.sort() сортує масив “на місці”, змінюючи в ньому порядок елементів.

Він повертає відсортований масив, але зазвичай повернене значення ігнорується, оскільки змінюється сам arr.

Наприклад:

let arr = [ 1, 2, 15 ];

// метод сортує вміст arr
arr.sort();

alert( arr );  // 1, 15, 2

Чи не помітили нічого дивного в цьому прикладі?

Порядок став 1, 15, 2. Це неправильно! Але чому?

За замовчуванням елементи сортуються як рядки.

Буквально, елементи перетворюються в рядки при порівнянні. Для рядків застосовується лексикографічний порядок, і дійсно виходить, що "2"> "15".

Щоб використовувати наш власний порядок сортування, нам потрібно надати функцію як аргумент arr.sort().

Функція має порівняти два довільних значення та повернути:

function compare(a, b) {
  if (a > b) return 1; // якщо перше значення більше за друге
  if (a == b) return 0; // якщо значення рівні
  if (a < b) return -1; // якщо перше значення меньше за друге
}

Наприклад, для сортування чисел:

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

Тепер все працює як треба.

Візьмімо паузу і подумаємо, що ж відбувається. Згаданий раніше масив arr може бути масивом чого завгодно, вірно? Він може містити числа, рядки, обʼєкти або щось ще. У нас є набір якихось елементів. Щоб впорядкувати його, нам потрібна функція, яка визначає порядок, яка знає, як порівнювати його елементи. За замовчуванням елементи сортуються як рядки.

Метод arr.sort(fn) реалізує загальний алгоритм сортування. Нам не потрібно піклуватися про те, як він працює всередині (в більшості випадків це оптимізоване швидке сортування чи Timsort). Реалізується прохід по масиву, порівнюються його елементи за допомогою наданої функції і змінюється їх порядок. Все, що залишається нам, це надати fn, яка робить це порівняння.

До речі, якщо ми коли-небудь захочемо дізнатися, які елементи порівнюються – ніщо не заважає нам вивести їх на екран:

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
  return a - b;
});

В процесі роботи алгоритм може порівнювати елемент з іншими по кілька разів, але він намагається зробити якомога менше порівнянь.

Функція порівняння може повернути будь-яке число

Насправді від функції порівняння потрібно будь-яке позитивне число, щоб сказати «більше», і негативне число, щоб сказати «менше».

Це дозволяє писати коротші функції:

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15
Краще використовувати стрілочні функції

Памʼятаєте стрілкові функції? Можна використовувати їх тут для того, щоб сортування виглядало більш акуратним:

arr.sort( (a, b) => a - b );

Працюватиме точно так, як і довша версія вище.

Використовуйте localeCompare для рядків

Памʼятаєте алгоритм порівняння рядків? Він порівнює літери за їх кодами за замовчуванням.

Для багатьох алфавітів краще використовувати метод str.localeCompare для правильного сортування літер, як наприклад Ö.

Наприклад, давайте відсортуємо кілька країн німецькою мовою:

let countries = ['Österreich', 'Andorra', 'Vietnam'];

alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (не правильно)

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (правильно!)

reverse

Метод arr.reverse змінює порядок елементів в arr на зворотний.

Наприклад:

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

Він також повертає масив arr зі зміненим порядком елементів.

split та join

Ситуація з реального життя. Ми пишемо додаток для обміну повідомленнями, і відвідувач вводить імена тих, кому його відправити, через кому: Вася, Петя, Маша. Але нам-то набагато зручніше працювати з масивом імен, ніж з одним рядком. Як його отримати?

Метод str.split(delim) саме це і робить. Він розбиває рядок на масив по заданому роздільнику delim.

У прикладі нижче таким роздільником є ​​рядок з коми та пропуску.

let names = 'Вася, Петя, Маша';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // Повідомлення отримають: Вася (і інші імена)
}

У методу split є необовʼязковий другий числовий аргумент – обмеження на кількість елементів в масиві. Якщо їх більше, ніж вказано, то залишок масиву буде відкинутий. На практиці це рідко використовується:

let arr = 'Вася, Петя, Маша, Іван'.split(', ', 2);

alert(arr); // Вася, Петя
Розбивка на букви

Виклик split(s) з порожнім аргументом s розбиває рядок на масив букв:

let str = "test";

alert( str.split('') ); // t,e,s,t

Виклик arr.join(glue) робить в точності протилежне split. Він створює рядок з елементів arr, вставляючи glue між ними.

Наприклад:

let arr = ["Вася", "Петя", "Маша"];

let str = arr.join(';'); // обʼєднуємо масив в рядок за допомогою ";"

alert( str ); // Вася;Петя;Маша

reduce/reduceRight

Якщо нам потрібно перебрати масив – ми можемо використовувати forEach, for або for..of.

Якщо нам потрібно перебрати масив і повернути дані для кожного елемента – ми використовуємо map.

Методи arr.reduce та arr.reduceRight схожі на методи вище, але вони трохи складніші. Вони використовуються для обчислення якогось одного значення на основі всього масиву.

Синтаксис:

let value = arr.reduce(function(accumulator, item, index, array) {
  // ...
}, [initial]);

Функція застосовується по черзі до всіх елементів масиву і «переносить» свій результат на наступний виклик.

Аргументи:

  • accumulator – результат попереднього виклику цієї функції, дорівнює initial при першому виклику (якщо переданий initial),
  • item – черговий елемент масиву,
  • index – його індекс,
  • array – сам масив.

При виконанні функції результат її виклику на попередньому елементі масиву передається як перший аргумент.

Зрозуміти простіше, якщо думати про перший аргумент як «збирач» результатів попередніх викликів функції. Після закінчення він стає результатом reduce.

Звучить складно?

Цей метод найпростіше зрозуміти на прикладі.

Тут ми отримаємо суму всіх елементів масиву лише одним рядком:

let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

Тут ми використовували найбільш поширений варіант reduce, який використовує тільки 2 аргументи.

Давайте детальніше розберемо, як він працює.

  1. При першому запуску sum дорівнює initial (останній аргумент reduce), тобто 0, а current – перший елемент масиву, рівний 1. Таким чином, результат функції дорівнює 1.
  2. При другому запуску sum = 1, і до нього ми додаємо другий елемент масиву (2).
  3. При третьому запуску sum = 3, до якого ми додаємо наступний елемент, і так далі…

Потік обчислень виходить такий:

У вигляді таблиці, де кожен рядок – виклик функції на черговому елементі масиву:

sum current результат
перший виклик 0 1 1
другий виклик 1 2 3
третій виклик 3 3 6
четвертий виклик 6 4 10
пʼятий виклик 10 5 15

Тут чітко видно, як результат попереднього виклику передається в перший аргумент наступного.

Ми також можемо опустити початкове значення:

let arr = [1, 2, 3, 4, 5];

// прибрано початкове значення (немає 0 в кінці)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

Результат той самий. Це тому, що при відсутності initial в якості першого значення береться перший елемент масиву, а перебір стартує з другого.

Таблиця розрахунків така ж, як і вище, без першого рядка.

Але таке використання вимагає крайньої обережності. Якщо масив порожній, то виклик reduce без початкового значення видасть помилку.

Ось приклад:

let arr = [];

// Error: Reduce of empty array with no initial value
// якби існувало початкове значення, reduce повернув би його для порожнього масиву.
arr.reduce((sum, current) => sum + current);

Тому рекомендується завжди вказувати початкове значення.

Метод arr.reduceRight працює аналогічно, але проходить по масиву справа наліво.

Array.isArray

Масиви не мають окремого типу в Javascript. Вони засновані на обʼєктах.

Тому typeof не може відрізнити простий обʼєкт від масиву:

alert(typeof {}); // обʼєкт
alert(typeof []); // також обʼєкт

…Але масиви використовуються настільки часто, що для цього придумали спеціальний метод: Array.isArray(value). Він повертає true, якщо value – це масив, інакше false.

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true

Більшість методів підтримують “thisArg”

Майже всі методи масиву, які викликають функції – такі як find, filter, map, за винятком методу sort, приймають необовʼязковий параметр thisArg.

Цей параметр не пояснювався вище, оскільки дуже рідко використовується, але для кращого розуміння теми ми зобовʼязані його розглянути.

Ось повний синтаксис цих методів:

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg - це необовʼязковий останній аргумент

Значення параметра thisArg стає this для func.

Наприклад, ось тут ми використовуємо метод обʼєкта army як фільтр, і thisArg передає йому контекст:

let army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  }
};

let users = [
  {age: 16},
  {age: 20},
  {age: 23},
  {age: 30}
];

// знайти користувачів, для яких army.canJoin повертає true
let soldiers = users.filter(army.canJoin, army);

alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23

Якби ми в прикладі вище використовували просто users.filter(army.canJoin), то виклик army.canJoin був би в режимі окремої функції, з this=undefined. Це призвело б до помилки.

Виклик users.filter(army.canJoin, army) можна замінити на users.filter(user => army.canJoin(user)), який робить те ж саме. Останній запис використовується навіть частіше, оскільки стрілочна функція більш наочна.

Підсумки

Шпаргалка по методам масиву:

  • Для додавання/видалення елементів:

    • push(... items) – додає елементи до кінця,
    • arr.pop() – дістає елемент з кінця,
    • arr.shift() – дістає елемент з початку,
    • arr.unshift(...items) – додає елементи в початок.
    • splice(pos, deleteCount, ...items) – починаючи з індексу pos, видаляє deleteCount елементів та вставляє items.
    • slice(start, end) – створює новий масив, копіюючи в нього елементи з позиції start до end (не включаючи end).
    • concat(...items) – повертає новий масив: копіює всі члени поточного масиву і додає до нього items. Якщо якийсь із items є масивом, тоді беруться його елементи.
  • Для пошуку серед елементів:

    • indexOf/lastIndexOf(item, pos) – шукає item, починаючи з позиції pos, і повертає його індекс або -1, якщо нічого не знайдено.
    • includes(value) – повертає true, якщо в масиві є елемент value, в іншому випадку false.
    • find/filter(func) – фільтрує елементи через функцію і віддається перше/всі значення, при проходженні яких функція повертає true.
    • findIndex схожий на find, але повертає індекс замість значення.
  • Для перебору елементів:

    • forEach(func) – викликає func для кожного елемента. Нічого не повертає.
  • Для перетворення масиву:

    • map(func) – створює новий масив з результатів виклику func для кожного елемента.
    • sort(func) – сортує масив «на місці», а потім повертає його.
    • reverse() – «на місці» змінює порядок елементів на протилежний і повертає змінений масив.
    • split/join – перетворює рядок в масив і назад.
    • reduce(func, initial) – обчислює одне значення на основі всього масиву, викликаючи func для кожного елемента і передаючи проміжний результат між викликами.
  • Додатково:

    • Array.isArray(value) перевіряє, чи є value масивом, якщо так, повертає true, інакше false.

Зверніть увагу, що методи sort, reverse та splice змінюють поточний масив.

Вивчених нами методів досить в 99% випадків, але існують і інші.

Функція fn викликається для кожного елемента масиву, подібного до map. Якщо будь-які/усі результати є true, повертає true, інакше false.

Ці методи поводяться приблизно як оператори || та &&. Якщо fn повертає істинне значення, arr.some() негайно повертає true і припиняє ітерацію по решті елементів. Якщо fn повертає хибне значення, arr.every() негайно повертає false і припиняє ітерацію по решті елементів.

Ми можемо використовувати every для порівняння масивів:

function arraysEqual(arr1, arr2) {
  return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}

alert( arraysEqual([1, 2], [1, 2])); // true
  • arr.fill(value, start, end) – заповнює масив повторюваними value, починаючи з індексу start до end.

  • arr.copyWithin(target, start, end) – копіює свої елементи, починаючи з start і закінчуючи end, в власну позицію target (перезаписує існуючі).

  • arr.flat(depth)/arr.flatMap(fn) – створює новий, плоский масив з багатовимірного масиву.

Повний список є в довіднику MDN.

На перший погляд, може здатися, що існує дуже багато різних методів, які досить складно запамʼятати. Але це тільки так здається.

Уважно вивчіть шпаргалку, представлену вище, а потім, щоб попрактикуватися, вирішите завдання, запропоновані в цьому розділі. Так ви отримаєте необхідний досвід в правильному використанні методів масиву.

Кожного разу, коли вам буде необхідно щось зробити з масивом, а ви не знаєте, як це зробити – приходьте сюди, дивіться на таблицю і шукайте правильний метод. Приклади допоможуть вам все зробити правильно, і незабаром ви швидко запамʼятайте методи без особливих зусиль.

Завдання

важливість: 5

Напишіть функцію camelize(str), яка перетворює такі рядки “my-short-string” в “myShortString”.

Тобто дефіси видаляються, а всі слова після них починаються з великої літери.

Приклади:

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

P.S. Підказка: використовуйте split, щоб розбити рядок на масив символів, потім переробіть все як потрібно та методом join зʼєднайте елементи в рядок.

Відкрити пісочницю з тестами.

function camelize(str) {
  return str
    .split('-') // розбиваємо 'my-long-word' на масив елементів ['my', 'long', 'word']
    .map(
      // робимо першу літеру велику для всіх елементів масиву, крім першого
      // конвертуємо ['my', 'long', 'word'] в ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // зʼєднуємо ['my', 'Long', 'Word'] в 'myLongWord'
}

Відкрити рішення із тестами в пісочниці.

важливість: 4

Напишіть функцію filterRange(arr, a, b), яка приймає масив arr, шукає в ньому елементи більші-рівні a та менші-рівні b і віддає масив цих елементів.

Функція повинна повертати новий масив і не змінювати вихідний.

Наприклад:

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (відфільтровані значення)

alert( arr ); // 5,3,8,1 (не змінюється)

Відкрити пісочницю з тестами.

function filterRange(arr, a, b) {
  // навколо виразу додано дужки для кращої читабельності
  return arr.filter(item => (a <= item && item <= b));
}

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (відфільтровані значення)

alert( arr ); // 5,3,8,1 (не змінюється)

Відкрити рішення із тестами в пісочниці.

важливість: 4

Напишіть функцію filterRangeInPlace(arr, a, b), яка приймає масив arr і видаляє з нього всі значення крім тих, які знаходяться між a і b. Тобто, перевірка має вигляд a ≤ arr[i] ≤ b.

Функція повинна змінювати поточний масив і нічого не повертати.

Наприклад:

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // видаляє всі числа крім тих, що в діапазоні від 1 до 4

alert( arr ); // [3, 1]

Відкрити пісочницю з тестами.

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // видаляти, якщо не у вказаному діапазоні
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // видаляє всі числа крім тих, що в діапазоні від 1 до 4

alert( arr ); // [3, 1]

Відкрити рішення із тестами в пісочниці.

важливість: 4
let arr = [5, 2, 1, -10, 8];

// ... ваш код для сортування за спаданням

alert( arr ); // 8, 5, 2, 1, -10
let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );
важливість: 5

У нас є масив рядків arr. Потрібно отримати відсортовану копію та залишити arr незміненим.

Створіть функцію copySorted(arr), яка буде повертати таку копію.

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS (без змін)

Для копіювання масиву використовуємо slice() і тут же – сортування:

function copySorted(arr) {
  return arr.slice().sort();
}

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
важливість: 5

Створіть функцію-конструктор Calculator, яка створює «розширюваний» обʼєкт калькулятора.

Завдання складається з двох частин.

  1. По-перше, реалізуйте метод calculate(str), який приймає рядок типу "1 + 2" в форматі «ЧИСЛО оператор ЧИСЛО» (розділені пробілами) і повертає результат. Метод повинен розуміти плюс + і мінус -.

    Приклад використання:

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  2. Потім додайте метод addMethod(name, func), який додає в калькулятор нові операції. Він приймає оператор name і функцію з двома аргументами func(a, b), яка описує його.

    Наприклад, давайте додамо множення *, ділення / і зведення в ступінь **:

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8
  • Для цього завдання не потрібні дужки або складні вирази.
  • Числа і оператор розділені рівно одним пропуском.
  • Не зайвим буде додати обробку помилок.

Відкрити пісочницю з тестами.

  • Зверніть увагу, як зберігаються методи. Вони просто додаються до внутрішнього обʼєкта (this.methods).
  • Всі тести та числові перетворення виконуються в методі calculate. У майбутньому він може бути розширений для підтримки складніших виразів.
function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2];

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  };

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

Відкрити рішення із тестами в пісочниці.

важливість: 5

У вас є масив об’єктів user, і в кожному з них є user.name. Напишіть код, який перетворює їх в масив імен.

Наприклад:

let ivan = { name: "Іван", age: 25 };
let petro = { name: "Петро", age: 30 };
let mariya = { name: "Марія", age: 28 };

let users = [ ivan, petro, mariya ];

let names = /* ... ваш код */

alert( names ); // Іван, Петро, Марія
let ivan = { name: "Іван", age: 25 };
let petro = { name: "Петро", age: 30 };
let mariya = { name: "Марія", age: 28 };

let users = [ ivan, petro, mariya ];

let names = users.map(item => item.name);

alert( names ); // Іван, Петро, Марія
важливість: 5

У вас є масив обʼєктів user, і у кожного з обʼєктів є name, surname та id.

Напишіть код, який створить ще один масив обʼєктів з параметрами id й fullName, де fullName – складається з name та surname.

Наприклад:

let ivan = { name: "Іван", surname: "Іванко", id: 1 };
let petro = { name: "Петро", surname: "Петренко", id: 2 };
let mariya = { name: "Марія", surname: "Мрійко", id: 3 };

let users = [ ivan, petro, mariya ];

let usersMapped = /* ... ваш код ... */

/*
usersMapped = [
  { fullName: "Іван Іванко", id: 1 },
  { fullName: "Петро Петренко", id: 2 },
  { fullName: "Марія Мрійко", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // Іван Іванко

Отже, насправді вам потрібно трансформувати один масив обʼєктів в інший. Спробуйте використовувати =>. Це невелика хитрість.

let ivan = { name: "Іван", surname: "Іванко", id: 1 };
let petro = { name: "Петро", surname: "Петренко", id: 2 };
let mariya = { name: "Марія", surname: "Мрійко", id: 3 };

let users = [ ivan, petro, mariya ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "Іван Іванко", id: 1 },
  { fullName: "Петро Петренко", id: 2 },
  { fullName: "Марія Мрійко", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // Іван Іванко

Зверніть увагу, що для стрілкових функцій ми повинні використовувати додаткові дужки.

Ми не можемо написати ось так:

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

Як ми памʼятаємо, є дві функції зі стрілками: без тіла value => expr та з тілом value => {...}.

Тут JavaScript трактуватиме { як початок тіла функції, а не початок обʼєкта. Щоб обійти це, потрібно укласти їх в круглі дужки:

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

Тепер усе добре.

важливість: 5

Напишіть функцію sortByAge(users), яка приймає масив обʼєктів з властивістю age і сортує їх по ньому.

Наприклад:

let ivan = { name: "Іван", age: 25 };
let petro = { name: "Петро", age: 30 };
let mariya = { name: "Марія", age: 28 };

let arr = [ petro, ivan, mariya ];

sortByAge(arr);

// now: [ivan, mariya, petro]
alert(arr[0].name); // Іван
alert(arr[1].name); // Марія
alert(arr[2].name); // Петро
function sortByAge(arr) {
  arr.sort((a, b) => a.age - b.age);
}

let ivan = { name: "Іван", age: 25 };
let petro = { name: "Петро", age: 30 };
let mariya = { name: "Марія", age: 28 };

let arr = [ petro, ivan, mariya ];

sortByAge(arr);

// тепер відсортовано: [ivan, mariya, petro]
alert(arr[0].name); // Ivan
alert(arr[1].name); // Mariya
alert(arr[2].name); // Petro
важливість: 3

Напишіть функцію shuffle(array), яка перемішує (випадковим чином переставляє) елементи масиву.

Багаторазові прогони через shuffle можуть привести до різних послідовностей елементів. Наприклад:

let arr = [1, 2, 3];

shuffle(arr);
// arr = [3, 2, 1]

shuffle(arr);
// arr = [2, 1, 3]

shuffle(arr);
// arr = [3, 1, 2]
// ...

Всі послідовності елементів повинні мати однакову ймовірність. Наприклад, [1,2,3] може бути перемішана як [1,2,3] або [1,3,2], або [3,1,2] тощо, з однаковою ймовірністю кожного випадку.

Простим рішенням може бути:

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

let arr = [1, 2, 3];
shuffle(arr);
alert(arr);

Це, звичайно, буде працювати, тому що Math.random() - 0.5 віддає випадкове число, яке може бути позитивним або негативним, отже, функція сортування змінює порядок елементів випадковим чином.

Але оскільки метод sort не призначений для використання в таких випадках, не всі можливі варіанти мають однакову ймовірність.

Наприклад, розглянемо код нижче. Він запускає shuffle 1000000 раз та підраховує ймовірність появи для всіх можливих варіантів arr:

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

// підрахунок імовірностей для всіх можливих варіантів
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// показати кількість всіх можливих варіантів
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

Результат прикладу (залежить від рушія JS):

123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223

Тепер ми чітко бачимо відхилення: 123 й 213 зʼявляються набагато частіше, ніж інші варіанти.

Результати цього коду можуть варіюватися при запуску на різних движках JavaScript, але очевидно, що такий підхід не надійний.

Так чому це не працює? Якщо говорити простими словами, то sort це “чорний ящик”: ми кидаємо в нього масив і функцію порівняння, чекаючи отримати відсортований масив. Але через абсолютну хаотичності порівнянь чорний ящик божеволіє, і як саме він божеволіє, залежить від конкретної його реалізації, яка різна в різних двигунах JavaScript.

Є й інші хороші способи розвʼязувати цю задачу. Наприклад, є відмінний алгоритм під назвою Тасування Фішера-Єйтса. Суть полягає в тому, щоб проходити по масиву у зворотному порядку і міняти місцями кожен елемент з випадковим елементом, який знаходиться перед ним.

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // випадковий індекс від 0 до i

    // поміняти елементи місцями
    // ми використовуємо для цього синтаксис "деструктивне присвоєння"
    // докладніше про нього - в наступних розділах
    // те ж саме можна записати як:
    // let t = array[i]; array[i] = array[j]; array[j] = t
    [array[i], array[j]] = [array[j], array[i]];
  }
}

Перевіримо цю реалізацію на тому ж прикладі:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// підрахунок імовірності для всіх можливих варіантів
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// показати кількість всіх можливих варіантів
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

Приклад виведення:

123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316

Тепер все в порядку: всі варіанти зʼявляються з однаковою ймовірністю.

Крім того, якщо подивитися з точки зору продуктивності, то алгоритм “Тасування Фішера-Єйтса” набагато швидший, оскільки в ньому немає зайвих витрат на сортування.

важливість: 4

Напишіть функцію getAverageAge(users), яка приймає масив об’єктів з властивістю age та повертає середній вік.

Формула обчислення середнього арифметичного значення: (age1 + age2 + ... + ageN) / N.

Наприклад:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
function getAverageAge(users) {
  return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}

let ivan = { name: "Іван", age: 25 };
let petro = { name: "Петро", age: 30 };
let mariya = { name: "Марія", age: 29 };

let arr = [ ivan, petro, mariya ];

alert( getAverageAge(arr) ); // 28
важливість: 4

Нехай arr – масив рядків.

Напишіть функцію unique(arr), яка повертає масив, що містить тільки унікальні елементи arr.

Наприклад:

function unique(arr) {
  /* ваш код */
}

let strings = ["Привіт", "Світ", "Привіт", "Світ",
  "Привіт", "Привіт", "Світ", "Світ", ":-O"
];

alert( unique(strings) ); // Привіт, Світ, :-O

Відкрити пісочницю з тестами.

Давайте пройдемося по елементам масиву:

  • Для кожного елемента ми перевіримо, чи є він в масиві з результатом.
  • Якщо є, то ігноруємо його, а якщо немає – додаємо до результатів.
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Привіт", "Світ", "Привіт", "Світ",
  "Привіт", "Привіт", "Світ", "Світ", ":-O"
];

alert( unique(strings) ); // Привіт, Світ, :-O

Код працює, але в ньому є потенційна проблема з продуктивністю.

Метод result.includes(str) всередині себе обходить масив result і порівнює кожен елемент з str, щоб знайти збіг.

Таким чином, якщо result містить 100 елементів і жоден з них не збігається з str, тоді він обійде весь result і зробить рівно 100 порівнянь. А якщо result великий масив, наприклад, 10000 елементів, то буде зроблено 10000 порівнянь.

Само собою це не проблема, адже рушій JavaScript дуже швидкий, тому обхід 10000 елементів масиву займає лічені мікросекунди.

Але ми робимо таку перевірку для кожного елемента arr в циклі for.

Тому, якщо arr.length дорівнює 10000, у нас буде щось на зразок 10000*10000 = 100 мільйонів порівнянь. Це забагато.

Ось чому дане рішення підходить тільки для невеликих масивів.

Далі в розділі Map та Set ми побачимо, як його оптимізувати.

Відкрити рішення із тестами в пісочниці.

важливість: 4

Припустимо, ми отримали масив користувачів у вигляді {id:..., name:..., age:...}.

Створіть функцію groupById(arr), яка створює з масиву об’єкт із ключом id та елементами масиву як значеннями.

Наприклад:

let users = [
  {id: 'іван', name: "Іван Іванко", age: 20},
  {id: 'ганна', name: "Ганна Іванко", age: 24},
  {id: 'петро', name: "Петро Петренко", age: 31},
];

let usersById = groupById(users);

/*
// після виклику функції ви повинні отримати:

usersById = {
  іван: {id: 'іван', name: "Іван Іванко", age: 20},
  ганна: {id: 'ганна', name: "Ганна Іванко", age: 24},
  петро: {id: 'петро', name: "Петро Петренко", age: 31},
}
*/

Така функція дійсно зручна при роботі з даними сервера.

У цьому завданні ми вважаємо, що id унікальний. Не може бути двох елементів масиву з однаковими id.

Будь ласка, використовуйте метод масиву .reduce у рішенні.

Відкрити пісочницю з тестами.

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

Відкрити рішення із тестами в пісочниці.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)