СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Объединение JavaScript массивов в черновиках Перевод

Это простой пост о техниках в JavaScript. Мы рассмотрим различные способы объединения\слияния двух JS массивов, их плюсы и минусы.

Наиболее распространенным подходом является Array#concat, который создает новые массив. Выглядит просто, но таит большие проблемы если ваши массивы большого объема. Под катом другие способы слияния массивов учитывающие эту и другие проблемы.

var a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
var b = [ "foo", "bar", "baz", "bam", "bun", "fun" ];


Простое слияние a и b, очевидно, должно быть таким:
[
   1, 2, 3, 4, 5, 6, 7, 8, 9,
   "foo", "bar", "baz", "bam" "bun", "fun"
]


Concat(..)


Наиболее распространенным подходом является такой:
var c = a.concat( b );

a; // [1,2,3,4,5,6,7,8,9]
b; // ["foo","bar","baz","bam","bun","fun"]

c; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]


Как вы можете видеть, c совершенно новый массив, который представляет собой комбинацию из двух массивов a и b, оставляя a и b нетронутыми. Просто, не так ли?

Но что делать, если в каждом массиве по 10000 элементов? с теперь состоит из 20000 элементов, что удваивается использованием памяти оставшимися a и b.

«Нет проблем», отвечаете Вы. Мы просто установим a и b в null и сборщик мусора заберет их, не так ли? Проблема решена!

a = b = null; // `a` и `b` могут теперь уйти


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

Циклические вставки


Ок, давайте просто добавлять один массив на содержимое другого используя Array#push(..):

for (var i=0; i < b.length; i++) {
    a.push( b[i] );
}

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

b = null;


Теперь содержимое a есть исходное плюс содержимое b.

Но что, если a был небольшим, а b был действительно большой? По причинам экономии памяти и времени, вы, вероятно, хотите положить a в начало b. Нет проблем, просто замените push(...) на unshift(..) и выполните цикл в обратном направлении:

for (var i=a.length-1; i >= 0; i--) {
    b.unshift( a[i] );
}

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

a = null;


Функциональные трюки


К сожалению, циклы for уродливы и тяжелы в поддержке. Но можем ли мы сделать лучше?

Вот наша первая попытка, используя Array#reduce:

// b в a
a = b.reduce( function(coll,item){
    coll.push( item );
    return coll;
}, a );

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

// или a в b
b = a.reduceRight( function(coll,item){
    coll.unshift( item );
    return coll;
}, b );

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]


Array#reduce(..) и Array#reduceRight(..) хороши, но они немного неуклюжи. ES6 со стрелочными функциями способно сделать их немного изящнее, но по прежнему требуются функции возвращающие значения.

Что скажете об этом:

// b в a
a.push.apply( a, b );

a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]

// или a в b
b.unshift.apply( b, a );

b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]


Это намного лучше, не правда ли?! Тем более, что здесь не нужно беспокоится об обратном порядке. Со spread-оператором из ES6 все становится еще красивее: a.push( ...b ) и b.unshift( ...a ).

Но не все так радужно, как может показаться. В обоих случаях, когда вы проходите либо a, либо b, второй аргумент apply означает, что массив сейчас является аргументом функции. Таким образом мы удваиваем размер занимаемой памяти (временно, конечно!), по существу копируя содержимое массива в стек для вызова функции. Кроме того, различные JavaScript движки имеют различные ограничения на количество аргументов, которые могут быть переданы.

Так, если массив содержит миллион элементов, вы почти наверняка превысите допустимые пределы. Это будет прекрасно работать на несколько тысяч элементов, но вы должны быть осторожны, чтобы не превышать безопасный предел.

Одним из вариантов было бы разбить массив на сегменты безопасной величины:

function combineInto(a,b) {
    var len = a.length;
    for (var i=0; i < len; i=i+5000) {
        b.unshift.apply( b, a.slice( i, i+5000 ) );
    }
}


Но подождите, мы снова понижаем читабельность! Давайте остановимся раньше, чем полностью откажемся от достигнутых успехов.

Итого


Array#concat(..) это хороший подход для объединения двух (или более!) массивов. Но скрытая опасность в том, что он создает новый массив, а не изменяет один из существующих. Существуют способы обойтись без дополнительного массива, но они имеют различные недостатки. Сравнивая рассмотренные подходы, пожалуй, лучше всего выглядит вариант с использованием reduce(..) и reduceRight(..).

Чтобы вы ни выбрали, это, вероятно, хорошая идея, но посмотрите критически на свой способ объединения массивов, прежде чем принимать его как должное.

комментарии (3)

+2
atd ,  
Какой ужас! О боже-ж мой!

Автор (исходной статьи) просто расписался в собственной неадекватности и непонимании, как работают рантаймы. Все эти размышления «а вот тут выделаяется лишний массив, а вот тут мы сделаем x=null и он тут же исчезнет» это просто дабл, нет, трипл фейспалм.

Особенно порадовал меня вариант с .unshift(), Кайл просто не понимает, что он на самом деле написал.
+2
atd ,  
P.S.: в комментах исходной статьи всё-таки нашёлся один адекватный человек, который понимает, что любые рассуждения о производительности без бенчмарков — полная профанация, и создал тест на jsperf:
jsperf.com/combining-js-arrays

Как мы видим, вариант с .unshift() занял последнее место, как и должен, а с .concat() всё в порядке, расходимся.
0
StreetStrider ,  
Array#concat(..) это хороший подход для объединения двух (или более!) массивов. Но скрытая опасность в том, что он создает новый массив, а не изменяет один из существующих.
Как будто что-то плохое.