Блог Конфуция
Array#sort() 02.09.2009

Так как во время сортировки массива (а также обращения) яваскрипт не создает его копию, сортировать числа можно очень быстро. И это здорово — но скучно.

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

Многообразие

Предположим, что нам нужно отсортировать массив объектов {x: число, y: число, z: число} сначала по x, потом по y потом по z. Есть как минимум три очевидных способа.

Каскадный:

array.sort(byZ).sort(byY).sort(byX)
Так можно делать, когда интерпретатор поддерживает «stable sort» и сортировки не связаны (тут так и есть).
Одиночный:

array.sort(function (a, b) { return byX(a, b) || byY(a, b) || byZ(a, b) })
UPD 2010-01-12: Конструкция хитрая, но работает. В яваскрипте (и еще, как минимум, в перле, руби, питоне и Си) оператор || не приводит аргументы к булеву типу, что позволяет клеить такие цепочки хоть до посинения.
Раскрытый (он же «inlined»):

array.sort(function (a, b) { return a.x - b.x || a.y - b.y || a.z - b.z })

В хороших и зрелых браузерах они все работают быстро и сортируют одинаково правильно (ага!). Но в нехороших браузерах, что-то подтормаживает, а что-то даже и не сортирует.

Кросбраузерность и скорость

Каскадный способ очень красив и даже не очень тупит, но не работает в Опере (включая зарелизенную десятку) и в Хроме (включая альфы четверки, они об этом знают). В третьем фаерфоксе, во всех ИЕ, а также в сафари три типа сортировки дают правильный порядок элементов. То есть придется париться с обертками.

Тесты показывают, что одиночный sort() почти в два раза быстрее каскадного. Раскрытый — еще быстрее раза в полтора-два-три (даже в хроме с его мегаумным В8). Забавно, что каскадный и одиночный способы сортировки дают одинаковые результаты в ИЕ 6 и 7 (в восьмом картина такая же как и в других браузерах).

Тестим

var messed =
[
	{x: 2, y: 0, z: 0},
	{x: 2, y: 0, z: 1},
	{x: 2, y: 0, z: 2},
	{x: 2, y: 1, z: 0},
	{x: 2, y: 1, z: 1},
	{x: 2, y: 1, z: 2},
	{x: 2, y: 2, z: 0},
	{x: 2, y: 2, z: 1},
	{x: 2, y: 2, z: 2},
	
	{x: 0, y: 0, z: 0},
	{x: 0, y: 0, z: 1},
	{x: 0, y: 0, z: 2},
	{x: 0, y: 1, z: 0},
	{x: 0, y: 1, z: 1},
	{x: 0, y: 1, z: 2},
	{x: 0, y: 2, z: 0},
	{x: 0, y: 2, z: 1},
	{x: 0, y: 2, z: 2},
	
	{x: 1, y: 0, z: 0},
	{x: 1, y: 0, z: 1},
	{x: 1, y: 0, z: 2},
	{x: 1, y: 1, z: 0},
	{x: 1, y: 1, z: 1},
	{x: 1, y: 1, z: 2},
	{x: 1, y: 2, z: 0},
	{x: 1, y: 2, z: 1},
	{x: 1, y: 2, z: 2}
]

var sorted =
[
	{x: 0, y: 0, z: 0},
	{x: 0, y: 0, z: 1},
	{x: 0, y: 0, z: 2},
	{x: 0, y: 1, z: 0},
	{x: 0, y: 1, z: 1},
	{x: 0, y: 1, z: 2},
	{x: 0, y: 2, z: 0},
	{x: 0, y: 2, z: 1},
	{x: 0, y: 2, z: 2},
	
	{x: 1, y: 0, z: 0},
	{x: 1, y: 0, z: 1},
	{x: 1, y: 0, z: 2},
	{x: 1, y: 1, z: 0},
	{x: 1, y: 1, z: 1},
	{x: 1, y: 1, z: 2},
	{x: 1, y: 2, z: 0},
	{x: 1, y: 2, z: 1},
	{x: 1, y: 2, z: 2},
	
	{x: 2, y: 0, z: 0},
	{x: 2, y: 0, z: 1},
	{x: 2, y: 0, z: 2},
	{x: 2, y: 1, z: 0},
	{x: 2, y: 1, z: 1},
	{x: 2, y: 1, z: 2},
	{x: 2, y: 2, z: 0},
	{x: 2, y: 2, z: 1},
	{x: 2, y: 2, z: 2}
]

function byX (a, b) { return a.x - b.x }
function byY (a, b) { return a.y - b.y }
function byZ (a, b) { return a.z - b.z }
function single (a, b) { return byX(a, b) || byY(a, b) || byZ(a, b) }
function combined (a, b) { return a.x - b.x || a.y - b.y || a.z - b.z }

var m = messed.slice()
console.time('multiple')
for (var i = 0; i < 1000; i++)
{
	m.sort(byZ),
	m.sort(byY),
	m.sort(byX)
}
console.timeEnd('multiple')

var s = messed.slice()
console.time('single')
for (var i = 0; i < 1000; i++)
{
	s.sort(single)
}
console.timeEnd('single')

var c = messed.slice()
console.time('combined')
for (var i = 0; i < 1000; i++)
{
	c.sort(combined)
}
console.timeEnd('combined')

Теги:
  • клиент
  • performance
  • sort
Очень жду ваших комментариев на почту или на гитхаб.