Несколько полезных кейсов при работе с массивами в JavaScript

Очень часто на тостере вижу вопросы вида «Как отсортировать массив в JavaScript по определенному правилу?», «Как сделать с массивом в JavaScript <действие>?» и т.д.

Под катом собраны некоторые манипуляции над массивами.

Преобразование массиво-подобного объекта в массив


В es2015 была добавлена замечательная функция Array.from, которая умеет преобразовывать в массив все, что похоже на массив. Принцип ее работы следующий:

  1. Если в передаваемом объекте в свойстве [Symbol.iterator] есть функция, она будет использована как генератор для наполнения массива
  2. Если в передаваемом объекте есть свойство length, то массив будет составлен из целочисленных индексов объекта от 0 до (object.length — 1)
  3. В других случаях она вернет пустой массив

Использование:
var arr = Array.from(obj);

Из стандартных объектов массиво-подобными считаются строки (разбиваются посимвольно), генераторы, объекты класса Set, arguments и некоторые другие

Сумма и произведение массива


var arr = [1, 2, 3, 4, 5];
var sum = arr.reduce((a, b) => a + b, 0); // 15
var prod = arr.reduce((a, b) => a * b, 1); // 120

Но с суммой нужно быть аккуратнее, если в массиве встретится не числовое значение, то оно будет преобразовано в строку, и начиная с него пойдет конкатенация строк, чтобы этого избежать можно использовать более безопасный вариант:

var sum = arr.reduce((a, b) => a + (+b || 0), 0);

Поиск в массиве и фильтрация массива


Многие знают о таком замечательном методе как indexOf, который ищет в массиве переданное в первом аргументе значение по точному соответствию (value === element) и возвращает индекс первого совпадения или -1 если ничего не найдено. Так же вторым аргументом можно передать индекс, с которого нужно начать поиск.

Есть похожий на него метод lastIndexOf, работающий аналогично, только поиск производится с конца массива. Но бывают ситуации, когда поиск по точному соответствию не подходит, для этих случаев существуют методы find и findIndex работают они похожим образом, вызывая для каждого элемента функцию, переданную в первом аргументе с параметрами (element, index, array). Поиск осуществляется до тех пор, пока функция не вернет true. find возвращает сам найденный элемент или undefined если ничего не найдено, а findIndex его индекс или -1 соответственно.

Пример, найдем в массиве первый элемент, который больше 5:

var arr = [1, 4, 2, 8, 2, 9, 7];
var elem = arr.find(e => e > 5); // 8
var index = arr.findIndex(e => e > 5); // 3

Еще одна частая задача, это фильтрация массива. Для этих целей существует метод filter, который возвращает новый массив, состоящий только из тех элементов, для которых функция переданная в первом аргументе вернула true:

var arr = [1, 4, 2, 8, 2, 9, 7];
var filtredArr = arr.filter(e => e > 5); // [8, 9, 7]

Так же иногда бывает нужно просто проверить элементы массива на соответствие некоторому условию, для этого существуют методы some и every, как и предыдущие методы они работают с функцией переданной в первом аргументе:

some возвращает true, если хотя бы для одного аргумента функция вернула true, и false в противном случае
every возвращает false, если хотя бы для одного аргумента функция вернула false, и true в противном случае

Обе останавливают поиск, когда искомое значение найдено. Это можно использовать несколько нестандартно, как известно метод forEach перебирает все элементы массива, но его работу невозможно прервать, однако это становится возможным благодаря методу some:

var arr = [1, 4, 2, 8, 2, 9, 7];
arr.some(function(element, index) {
    console.log(index + ': ' + element);
    if(element === 8) {
        return true; //прерываем выполнение
    }
});
/* В консоли увидим:
0: 1
1: 4
2: 2
3: 8
*/

Сортировка массивов


Для сортировки массива используется метод sort. По умолчанию все элементы сортируются как строки по возрастанию кодов utf-16. Стандартное поведение можно изменить передав первым аргументом функцию-компаратор. Компаратор — это такая функция, которая получает на вход два аргумента (a, b) и должна вернуть: -1, если a идет раньше чем b; 1, если a идет позже чем b; 0, если порядок не важен, то есть аргументы равны. Метод sort довольно лоялен к компаратору и принимает на выходе любые значения меньше 0 как -1, а значения больше 0 как 1.

Важно! Метод sort, хотя и возвращает результат, все операции проводит над исходным массивом. Если необходимо оставить исходный массив без изменений, можно воспользоваться следующим приемом:

var sortedArr = arr.slice().sort();

Так как метод slice без аргументов возвращает клон массива, метод sort будет работать с этим клоном и в результате его вернет.

Простая сортировка, сравнение элементов как числа

arr.sort((a, b) => a - b); //по возрастанию
arr.sort((a, b) => b - a); //по убыванию

Типобезопасная сортировка

arr.sort((a, b) => (a < b && -1) || (a > b && 1) || 0); //по возрастанию
arr.sort((a, b) => (a < b && 1) || (a > b && -1) || 0); //по убыванию

Сортировка массива объектов по их свойствам

Более частая ситуация возникает, когда нужно отсортировать массив однотипных объектов по их свойству, а то и нескольким свойствам сразу, для этого удобно иметь функцию, которая создавала бы нам компаратор на любой случай жизни:

function compare(field, order) {
    var len = arguments.length;
    if(len === 0) {
        return (a, b) => (a < b && -1) || (a > b && 1) || 0;
    }
    if(len === 1) {
        switch(typeof field) {
            case 'number':
                return field < 0 ?
                    ((a, b) => (a < b && 1) || (a > b && -1) || 0) :
                    ((a, b) => (a < b && -1) || (a > b && 1) || 0);
            case 'string':
                return (a, b) => (a[field] < b[field] && -1) || (a[field] > b[field] && 1) || 0;
        }
    }
    if(len === 2 && typeof order === 'number') {
        return order < 0 ?
            ((a, b) => (a[field] < b[field] && 1) || (a[field] > b[field] && -1) || 0) :
            ((a, b) => (a[field] < b[field] && -1) || (a[field] > b[field] && 1) || 0);
    }
    var fields, orders;
    if(typeof field === 'object') {
        fields = Object.getOwnPropertyNames(field);
        orders = fields.map(key => field[key]);
        len = fields.length;
    } else {
        fields = new Array(len);
        orders = new Array(len);
        for(let i = len; i--;) {
            fields[i] = arguments[i];
            orders[i] = 1;
        }
    }
    return (a, b) => {
        for(let i = 0; i < len; i++) {
            if(a[fields[i]] < b[fields[i]]) return orders[i];
            if(a[fields[i]] > b[fields[i]]) return -orders[i];
        }
        return 0;
    };
}

//Использование
arr.sort(compare()); //Обычная типобезопасная сортировка по возрастанию
arr.sort(compare(-1)); //Обычная типобезопасная сортировка по убыванию
arr.sort(compare('field')); //Сортировка по свойству field по возрастанию
arr.sort(compare('field', -1)); //Сортировка по свойству field по убыванию
/* Сортировка сначала по полю field1
   при совпадении по полю field2, а если и оно совпало, то по полю field3
   все по возрастанию */
arr.sort(compare('field1', 'field2', 'field3'));
/* Сортировка сначала по полю field1 по возрастанию
   при совпадении по полю field2 по убыванию */
arr.sort(compare({
    field1 : 1,
    field2 : -1
}));

Сортировка подсчетом

Хотя метод sort работает достаточно быстро, на очень больших массивах его скорости может оказаться недостаточно, благо есть метод сортировки который показывает большую производительность при соблюдении 2х условий, а именно — массив достаточно большой (больше 100.000 элементов) и в массиве много повторяющихся значений. Называется он сортировкой подсчетом. Я представлю пример его реализации для сортировки по возрастанию массива из числовых элементов:

function sortCounts(arr) {
    var counts = arr.reduce((result, value) => {
        if(typeof result[value] === 'undefined') {
            result[value] = 0;
        }
        return ++result[value], result;
    }, {});
    var values = Object.getOwnPropertyNames(counts).sort((a, b) => a - b);
    var start = 0;
    for(let value of values) {
        let end = counts[value] + start;
        arr.fill(+value, start, end);
        start = end;
    }
}

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 36

    –10
    Использование лямбда выражений, на данный момент, поддерживается далеко не всеми браузерами.

    Поэтому, если вы уже пишите для новичков, пишите стандартным JS, который работает в том числе и в ИЕ.
      0
      Может будут какие то комментарии?
        +6
        Будут. Babel
          0
          Цитируя автора:

          Очень часто на тостере вижу вопросы вида «Как отсортировать массив в JavaScript по определенному правилу?», «Как сделать с массивом в JavaScript <действие>?» и т.д.

          И вы искренне считаете, что люди задающие такие вопросы, будут использовать Babel для преобразования ES 6? Честно честно?

          Если автор решил собрать что-то для начального уровня, то оно должно быть максимально разжёвано и работать "из коробки" а не из-под Babel.
            +2
            Знаете, это очень спорный вопрос. Лично я отношусь к babel в 2016 году как к чему-то типа компилятора для C++, т.е. как к неотъемлемой части разработки. Да, можно долго спорить, использовать его или нет, и все такое, но новичку уж проще один раз настроить бебель, чем постоянно смотреть, что какими браузерами поддерживается. Более того, почти наверное он насмотрелся на примеры на том же реакте, так что babel уже можно считать установленным (вряд ли кто-то начинает изучение js с сортировок массива, а какие-то reduce так вообще нужны один раз на 5 лет).

            Однако ориентироваться под старые браузеры и писать для начинающих статьи, ориентируясь на старые стандарты — это как изучать 3d max, только взять не актуальную версию, а, скажем, вторую или третью.

            Ну и, наконец, при этом — главное — рассказывать о методах, появившихся в es6, но не использовать arrow-нотацию из того же es6 как минимум странно. А то, что этот стандарт до сих пор поддерживают не все браузеры полностью — уже проблема браузеров.
              +3
              Я понимаю вашу позицию и частично ее принимаю — во второй части.
              Однако, когда автор пишете туториал, он не должен ориентироваться на себя. Мало ли какой стек у него стек. Это во-первых.
              А во-вторых, у новичков и так проблем выше крыши. Слишком многое еще нужно узнать и разобраться. И, если честно, Babel, не первый в этом списке.

              Так моя позиция более понятна?
                +1
                Кстати тот же reduce поддерживает ИЕ9, в отличии от лямбд.
                0
                Так из какой коробки? js на странице в броузере? В расширении браузера (три контекста — background, popup, content, под лисой еще bootstrap — причем в лисе это как три разных броузера)? Вебворкеры? Или мы на сервере под нодой? Или мы на постгресе в plv8? или мы в ноде но на этапе сборки проекта (webpack). Я уже молчу про экзотику типа фриды.

                что люди задающие такие вопросы, будут использовать Babel для преобразования ES 6? Честно честно?

                Если ответят бабелем — то многие будут. Вопросы ведь такие далеко не всегда от глупости задаются — кто то только вливается, кто то вообще только начинает программировать. Это все равно что смотреть на иностранца и думать что он тупой только потому что плохо разговаривает на чужом ему языке. Так и эти вопрошающие — для многих из них это просто пока еще незнакомая среда с неизвестными им правилами (общения, обучения, добывания информации)
              +2
              Javascript работает не только в браузерах
                +2
                Тем не менее, согласитесь, упоминания того, что данные методы являются стандартом (да и сам код примеров) ES6 не хватает.
                +1
                Будут. Стрелочные функции !== лямба-функции.
                • UFO just landed and posted this here
                    –1
                    Позволю себе скинуть ссылку, там достаточно подробно написано.
                    https://learn.javascript.ru/es-function

                    P.S. хабр мне не дает сделать ссылку активной.
                    • UFO just landed and posted this here
                        –1
                        > «И вы меня конечно простите, но в любом случае для меня MDN является ресурсом куда авторитетнее чем javascript.ru. Ребята из Mozilla участвуют в разработке стандарта языка (EcmaScript), и я верю что они лучше знают, что можно назвать „лямбдой“.»
                        Аналогично, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

                        > «Можете привести цитату? Может я внимательно прочитал?»
                        Рассказываются различия между короткой формой и обычной:
                        https://learn.javascript.ru/es-function#функции-стрелки-не-имеют-своего-this
                        https://learn.javascript.ru/es-function#функции-стрелки-не-имеют-своего-arguments

                        И цитата:
                        «Не имеют своих this и arguments, при обращении получают их из окружающего контекста.
                        Не могут быть использованы как конструкторы, с new.»

                        > «Я лично тоже придерживаюсь мнения, что лямбды это просто сокращенная версия анонимной функции, чем и является arrow function.»
                        Как писал Apathetic, поэтому стрелочные функции !== лямба-функции, т.е. не просто сокращенная версия.
                        • UFO just landed and posted this here
                            0
                            > «Как ваша цитата подтверждает утверждение, что стрелочная функция это не лямбда? То что вы приводите касается исключительно лексического this и коллекции arguments, по-вашему их отсутствие как-то влияет на то лямбда это или нет, вы это серьезно?»
                            Лямбдой была, лямбдой и осталась. Вы хоть понимаете, что я пытался донести? Всего лишь одну вещь — какие есть различия между стрелочной записью и обычной.

                            > «Вы еще ни разу не подкрепили свое утверждение хотя бы одним источником – одно голословное утверждение. Я уже привел как минимум три ссылки, где явным образом написано что arrow function === lambda и переняли это из C#, но вы все равно продолжаете глупо спорить.»
                            Мне кажется, или мы говорим о разных вещах? И наверняка о разном смысле различия, если вам так угодно. Список различий я вам привел.

                            А теперь сначала. Что arrow function, что lambda, anonymous function — это все лямбды. Стрелочная запись отличается по функционалу от полной записи лямбды. Поэтому использование стрелочной записи !== использованию обычной записи.

                            Цитируя самого себя:
                            > «Как писал Apathetic, поэтому стрелочные функции !== лямба-функции, т.е. не просто сокращенная версия.»
                            Понимая, то, как вы на это смотрите, фраза абсолютна не правильная. В данном случае, стрелочные функции === лямба-функции. Текстом в цитате, я подразумевал под 1 — короткую запись, под 2 длинную.

                            > "… Перестаньте жить в информационном вакууме и читайте хоть немного больше чем javascript.ru, на котором кстати по вашей ссылке ни слова про лямбды."
                            Источник не авторитетный, согласен. Тут уж извините, забыл где нахожусь и по привычке вставил первую ссылку. Кстати, которая полностью подтверждает мои суждения. Тем не менее, ссылку на реальный источник, в котором сказано ровно тоже самое — я вам предоставил. Остальной опус в мой адрес считаю, как минимум, неуместными предрассудками.

                            • UFO just landed and posted this here
                                +1
                                > Мой вопрос про разницу лямбд и arrow function был скорее чтобы навести автора комментария на правильное решение, а не чтобы тратить столько времени на бесполезные споры совсем о других вещах.

                                Согласен. Я неправильно понял вашу мысль, подумав, что Apathetic имел ввиду разницу между короткой записью и длинной.
                                  +1
                                  чтобы навести автора комментария на правильное решение
                                  И навели. Совместно с alexkunin вы помогли мне понять, что я был не прав. Спасибо =)
                        –2
                        Лямда — это анонимная функция, если упрощённо говорить, да.
                        Но стрелочные функции тут при чём? Вот вам пример стрелочной фунции, которая не является при этом анонимной:
                        var f = () => console.log('hello');

                        А вот пример анонимной:
                        setTimeout(() => console.log('hello'), 1000);
                        • UFO just landed and posted this here
                            –1
                            Вы правы, это действительно переменная, в которой хранится ссылка на функцию. Но у меня такое ощущение, будто вы думаете, что объявление функции через function declaration происходит как-то по-другому. Точно так же создаётся переменная, в которой сохраняется функция.
                            Функция — это объект, она принципиально не может храниться в переменных, и как вы её не объявляйте — через expression или declaration, в конечном счете разницы нет — интерпретатор создаёт переменную, в которой сохраняет ссылку на функцию.
                            UPD. Мне нужно время.
                            • UFO just landed and posted this here
                                +1
                                Спасибо, я был не прав.
                              • UFO just landed and posted this here
                                  0
                                  Именно поэтому я эту часть и удалил.
                                  • UFO just landed and posted this here
                            • UFO just landed and posted this here
                      • UFO just landed and posted this here
                      • UFO just landed and posted this here
                          –1
                          Согласно спецификации, если нет инициирующего значения, в качестве него берется нулевой элемент, а перебор массива начинается с первого. В v8 и SpiderMonkey это работает согласно спецификации. Если действительно с этим где-то есть проблемы, то поправлю.
                          • UFO just landed and posted this here
                              +1
                              Спасибо за замечание, про пустой массив как то не подумал, поправил
                          +1
                          Если это статья для новичков — где тут подробное объяснение, что происходит в каждой строчке?

                          var sum = [].reduce((a, b) => a + b);

                          Я, например, вот на этом месте уже начал сомневаться. Где результат данной операции с примером? Почему именно так, а не иначе?

                          Ну и т.д.
                            –3
                            Использую underscore для работы с массивами.

                            Only users with full accounts can post comments. Log in, please.