Начебто, елемент був видалений, але при перевірці виявляється, що масив все ще має 3 елементи arr.length == 3.
Це нормально, тому що все, що робить delete obj.key – це видаляє значення за ключем key. Це нормально для обʼєктів, але для масивів ми звичайно хочемо, щоб інші елементи змістились і зайняли місце, що звільнилося. Ми чекаємо, що масив стане коротшим.
Тому слід застосовувати спеціальні методи.
Метод arr.splice – це універсальний «швейцарський ніж» для роботи з масивами. Вміє все: додавати, видаляти і замінювати елементи.
Він змінює 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
Метод 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. Це часто використовують, щоб створити копію масиву для подальших перетворень, які не повинні змінювати вихідний масив.
Метод 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:
Методи arr.indexOf та arr.includes мають однаковий синтаксис і роблять по суті те ж саме, що і їх рядкові аналоги, але працюють з елементами замість символів:
arr.indexOf(item, from) – шукає item, починаючи з індексу from, і повертає індекс, на якому був знайдений шуканий елемент, в іншому випадку -1.
arr.includes(item, from) – шукає item, починаючи з індексу from, і повертає true, якщо пошук успішний.
Зазвичай ці методи використовуються лише з одним аргументом: item для пошуку. Типово пошук відбувається з самого початку.
Незначною, але вартою уваги властивістю includes є те, що він правильно обробляє NaN, на відміну від indexOf:
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (повинен бути 0, але === перевірка на рівність не працює з NaN)
alert( arr.includes(NaN) );// true (вірно)
Це пов’язано з тим, що метод includes був доданий до JavaScript значно пізніше і використовує більш сучасний алгоритм порівняння.
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
Метод 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
Виклик 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 (правильно!)
Ситуація з реального життя. Ми пишемо додаток для обміну повідомленнями, і відвідувач вводить імена тих, кому його відправити, через кому: Вася, Петя, Маша. Але нам-то набагато зручніше працювати з масивом імен, ніж з одним рядком. Як його отримати?
Метод str.split(delim) саме це і робить. Він розбиває рядок на масив по заданому роздільнику delim.
У прикладі нижче таким роздільником є рядок з коми та пропуску.
let names = 'Вася, Петя, Маша';
let arr = names.split(', ');
for (let name of arr) {
alert( `A message to ${name}.` ); // Повідомлення отримають: Вася (і інші імена)
}
У методу split є необовʼязковий другий числовий аргумент – обмеження на кількість елементів в масиві. Якщо їх більше, ніж вказано, то залишок масиву буде відкинутий. На практиці це рідко використовується:
Якщо нам потрібно перебрати масив – ми можемо використовувати 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 аргументи.
Давайте детальніше розберемо, як він працює.
При першому запуску sum дорівнює initial (останній аргумент reduce), тобто 0, а current – перший елемент масиву, рівний 1. Таким чином, результат функції дорівнює 1.
При другому запуску sum = 1, і до нього ми додаємо другий елемент масиву (2).
При третьому запуску 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 працює аналогічно, але проходить по масиву справа наліво.
Масиви не мають окремого типу в Javascript. Вони засновані на обʼєктах.
Тому typeof не може відрізнити простий обʼєкт від масиву:
alert(typeof {}); // обʼєкт
alert(typeof []); // також обʼєкт
…Але масиви використовуються настільки часто, що для цього придумали спеціальний метод: Array.isArray(value). Він повертає true, якщо value – це масив, інакше false.
Майже всі методи масиву, які викликають функції – такі як 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)), який робить те ж саме. Останній запис використовується навіть частіше, оскільки стрілочна функція більш наочна.
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 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'
}
Напишіть функцію 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]
Створіть функцію-конструктор Calculator, яка створює «розширюваний» обʼєкт калькулятора.
Завдання складається з двох частин.
По-перше, реалізуйте метод calculate(str), який приймає рядок типу "1 + 2" в форматі «ЧИСЛО оператор ЧИСЛО» (розділені пробілами) і повертає результат. Метод повинен розуміти плюс + і мінус -.
Приклад використання:
let calc = new Calculator;
alert( calc.calculate("3 + 7") ); // 10
Потім додайте метод 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
Для цього завдання не потрібні дужки або складні вирази.
У вас є масив об’єктів 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 ); // Іван, Петро, Марія
Напишіть функцію 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
Всі послідовності елементів повинні мати однакову ймовірність. Наприклад, [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]}`);
}
Тепер ми чітко бачимо відхилення: 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]}`);
}
Тепер все в порядку: всі варіанти зʼявляються з однаковою ймовірністю.
Крім того, якщо подивитися з точки зору продуктивності, то алгоритм “Тасування Фішера-Єйтса” набагато швидший, оскільки в ньому немає зайвих витрат на сортування.
Для кожного елемента ми перевіримо, чи є він в масиві з результатом.
Якщо є, то ігноруємо його, а якщо немає – додаємо до результатів.
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 ми побачимо, як його оптимізувати.
Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)
You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.