Преобразование массива
Перейдём к методам преобразования и упорядочения массива.
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)
реализует общий алгоритм сортировки. Нам не нужно заботиться о том, как он работает внутри (в большинстве случаев это оптимизированная быстрая сортировка). Она проходится по массиву, сравнивает его элементы с помощью предоставленной функции и переупорядочивает их. Всё, что остаётся нам, это предоставить fn
, которая делает это сравнение.
Кстати, если мы когда-нибудь захотим узнать, какие элементы сравниваются – ничто не мешает нам вывести их на экран:
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( 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 );
Будет работать точно так же, как и более длинная версия выше.
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( `Сообщение получат: ${name}.` ); // Сообщение получат: Вася (и другие имена)
}
У метода split
есть необязательный второй числовой аргумент – ограничение на количество элементов в массиве. Если их больше, чем указано, то остаток массива будет отброшен. На практике это редко используется:
let arr = 'Вася, Петя, Маша, Саша'.split(', ', 2);
alert(arr); // Вася, Петя
Разбивка по буквам
Вызов split(s)
с пустым аргументом s
разбил бы строку на массив букв:
let str = "тест";
alert( str.split('') ); // т,е,с,т
Вызов 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(previousValue, item, index, array) {
// ...
}, [initial]);
Функция применяется по очереди ко всем элементам массива и «переносит» свой результат на следующий вызов.
Аргументы:
previousValue
– результат предыдущего вызова этой функции, равен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 | result | |
---|---|---|---|
первый вызов | 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 работает аналогично, но проходит по массиву справа налево.