alexeykuzmin.com

About simple things

Массивы в JavaScript'е

Перевод статьи Dr. Axel Rauschmayer‘а “Arrays in JavaScript”.

Статья посвящена особенностям реализации массивов в JavaScript. Как ни странно, они мало чем отличаются от обычных объектов.

1. В двух словах

Как вы должно быть знаете, объекты в JavaScript’е это хэши, состоящие из пар ключ-значение, где в качестве ключей используются строки. Массивы – те же объекты, лишь с парой особенностей:

  1. Индексы массива: если ключ является строковым представлением неотрицательного целого числа меньше некоего фиксированного значения, то он трактуется как индекс массива.
  2. "length": значение этого свойства – неотрицательное целое, являющееся длиной массива. Длина массива определяется как числовое значение самого большого индекса плюс один.

То, что индексы массивов в JavaScript’е на самом деле являются строками, может вызвать недоумение, особенно если вы знакомы с другими языками. Конечно, JavaScript-движки внутри себя оптимизируют работу с массивами и используют числа в качестве индексов. Но в спецификации индексы описаны как строки и работать с ними приходится как со строками:

1
2
3
var arr = ['a', 'b', 'c'];
arr['0']  // 'a'
arr[0]  // 'a'

Так как 0 не является валидным идентификатором, использование точечной нотации (arr.0) вызывает синтаксическую ошибку, из-за чего приходится использовать квадратные скобки. Оператор квадратные скобки преобразует свой операнд в строку, поэтому arr[0] работает нормально, что и видно выше. Индекс элемента в массиве не может превышать 32 бита (примерно). Аналогично оператору квадратные скобки, оператор in также преобразует свой первый операнд в строку, поэтому можно использовать числа для того, чтобы проверить наличие в массиве элемента с данным индексом:

1
2
2 in [ 'a', 'b', 'c' ]  // true
3 in [ 'a', 'b', 'c' ]  // false

Дальше будет более подробно описано то, как устроены массивы в JavaScript’е.

2. Массивы с лакунами

Как уже было сказано, массивы являются хэшами, в которых ключами являются числа (представленные в виде строк), а значениями – произвольные величины. Из этого следует, что массив может содержать лакуны – индексы меньше длины массива, для которых нет значений. Лакуны можно создать с помощью литерала массива, не указывая значения между запятыми (но не между последней запятой и закрывающей скобкой). Лакуны пропускаются методами итерации по массиву, такими как forEach или map. Массив с лакунами называется разреженным (sparse). Сравним разреженный и плотный (dense) массивы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var sparse = [ , , 'c' ];
var dense  = [ undefined, undefined, 'c' ];

0 in sparse  // false
0 in dense  // true


for(var i=0; i < sparse.length; i++) console.log(sparse[i]);
// undefined
// undefined
// c
for(var i=0; i < dense.length; i++) console.log(dense[i]);
// undefined
// undefined
// c

sparse.forEach(function (x) { console.log(x) });
// c
dense.forEach(function (x) { console.log(x) });
// undefined
// undefined
// c

sparse.map(function (x,i) { return i });
// [ , , 2 ]
dense.map(function (x,i) { return i });
// [ 0, 1, 2 ]

sparse.filter(function () { return true })
// [ 'c' ]
dense.filter(function () { return true })
// [ undefined, undefined, 'c' ]

Подробнее про плотные и разреженные массивы можно прочитать в статье по ссылке [2].

3. Конструктор Array

Конструктор Array можно вызвать тремя способами:

  1. new Array(): создаёт новый пустой массив. Пустой литерал массива [] является более короткой записью этой же операции.
  2. new Array(len): создаёт массив с len лакунами. В некоторых JavaScript-движках эта операция ведёт к выделению памяти для массива, что улучшает производительность в случае небольших массивов (для больших это не так). Но в большинстве случаев производительность не важна и стоит избегать ненужного усложнения, связанного с выделением памяти. Если это возможно, лучше создать массив с помощью литерала массива, сразу указав все элементы.
  3. new Array(elem1, elem2, ...): создаёт массив из элементов elem1, elem2 и так далее. Это самый неудачный вариант использования конструктора Array, потому что если передать ему только один аргумент и он окажется целым числом, то реализуется сценарий номер 2, описанный выше. Чтобы обойти эту проблему, используйте литерал массива [ elem1, elem2, ...], который создаст массив без побочных эффектов.

Если вызвать Array как функцию (без оператора new), эффект будет таким же, как если вызвать его как конструктор. (К слову говоря, сейчас в JavaScript нет встроенной функции для надёжного создания массива из отдельных элементов, видимо из-за редкости ситуации, когда это нужно. В ECMAScript 6 появится метод Array.of(), который исправит положение.)

4. Индексы массива

Спецификация ECMAScript содержит чёткое определение того, какие имена ключей должны трактоваться как индексы массива. Строка s является индексом массива тогда и только когда:

  • s, интерпретированная как 32-битное целое без знака и преобразованное обратно в строку, совпадает с s.
  • s, преобразованная к целому, меньше 232 − 1 (максимально возможная длина).

Если сравнивать численно, индекс s должен находится в пределах 0 ≤ s < 2^32 − 1. Функция ToUint32() – преобразование в 32-битное целое без знака – определена внутри спецификации. На JavaScript’е её можно реализовать так [1]:

1
2
3
function ToUint32(x)
  return x >>> 0;
}

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

1
2
3
4
5
ToUint32('0')  // 0
ToUint32('00')  // 0
ToUint32('03')  // 3
ToUint32('abc')  // 0
ToUint32(Math.pow(2,32)+3)  // 3

Только первая строка удовлетворяет условию (1) и является валидным индексом. Любые строки, не удовлетворяющие условиям выше, трактуются как обычные имена свойств:

1
2
3
var arr = ['a', 'b', 'c'];
arr['0']  // 'a'
arr['00']  // undefined

5. length

Свойство length массива является неотрицательным целым l в интервале 0 ≤ l ≤ 2^32 − 1 (32 бита).

5.1 Отслеживание индексов

Когда в массив добавляются новые элементы, length автоматически увеличивается:

1
2
3
4
var arr = [];
arr.length  // 0
arr[0] = 'a';
arr.length  // 1

5.2 Уменьшение length

Если длине массива присвоить значение l', которое меньше текущего значения l, то из массива будут удалены элементы, индексы которых удовлетворяют условию l' ≤ i < l:

1
2
3
4
5
var arr = [ 'a', 'b', 'c' ];
arr.length  // 3
2 in arr  // true
arr.length = 2;  // 2
2 in arr  // false

5.3 Увеличение length

Если присвоить length значение больше текущего, то в массиве появятся лакуны:

1
2
3
var arr = ['a'];
arr.length = 3;
arr  // [ 'a', , ,]

5.4 Допустимые значения length

Свойству length можно присвоить произвольное значение, но преобразование этого значения в число к помощью функций ToUint32() и Number() обязаны давать одинаковый результат. Например:

1
2
3
4
5
6
ToUint32('0')  // 0 (*)
ToUint32('000')  // 0 (*)
ToUint32('-1')  // 4294967295
ToUint32(Math.pow(2,32)-1)  // 4294967295 (*)
ToUint32(Math.pow(2,32))  // 0
ToUint32('abc')  // 0

Все значения, помеченные звёздочкой, являются допустимыми, остальные – нет:

1
2
3
4
5
6
Number('0')  // 0
Number('000')  // 0
Number('-1')  // -1
Number(Math.pow(2,32)-1)  // 4294967295
Number(Math.pow(2,32))  // 4294967296
Number('abc')  // NaN

Это можно легко проверить:

1
2
3
[].length = -1  // RangeError: Invalid array length
[].length = Math.pow(2,32)  // RangeError: Invalid array length
[].length = 'abc'  // RangeError: Invalid array length

6. Экземпляры “класса” Array

Экземпляры “класса” Array являются обычными объектами, с единственным отличием: определение некоторых свойств происходит особым образом:

  • Индексы массива: значение length увеличивается, если это необходимо.
  • length: бросается исключение, если значение является недопустимым; удаляются элементы массива, если новое значение меньше текущего.

Все остальные свойства ведут себя точно также, как у обычных объектов. Обратите внимание, что определение свойств происходит и при использовании оператора присваивания (вызывается внутренний метод [[Put]], который, в свою очередь, вызовет [[DefineOwnProperty]], если в цепочке прототипов не найдётся сеттера для этого свойства).

Вышесказанное означает, что невозможно создать “подкласс” “класса” Array, используя стандартный ECMAScript 5. Подробнее об этом можно прочитать в статье “Subtyping JavaScript built-ins”.

7. Выходя за пределы

Что произойдёт, если попытаться обратиться к элементу с индексом, превосходящим максимально допустимое значение? Индекс будет интерпретирован как обычное имя свойства объекта. Если попытаться использовать такой индекс для добавления значения в массив, новый элемент массива не будет создан. Ниже иллюстрация использования слишком большого индекса:

1
2
3
4
5
6
7
8
var arr = ['a', 'b'];
arr[Math.pow(2,32)-1] = 'c';
arr
// [ 'a', 'b' ]
arr.length
// 2
arr[Math.pow(2,32)-1]
// 'c'

Если же попытаться добавить элемент в массив, уже имеющий максимальную длину, возникнет ошибка:

1
2
3
var arr = new Array(Math.pow(2,32)-1)  // максимально возможная длина
arr.push('x')
// RangeError: Invalid array length

8. Рекомендации

Рекомендации при работе с массивами:

  • Воспринимайте индексы массива как числа. Они обычно так и реализованы и, возможно, это станет стандартом в будущих версиях ECMAScript.
  • Старайтесь не использовать конструктор Array().
  • Используйте литерал массива везде, где это возможно.
  • Не стоит использовать необычные схемы работы с массивами. Если следовать стандартным паттернам, JavaScript-движки смогут лучше оптимизировать выполнение кода. В статье Криса Уилсона (Chris Wilson) “Performance Tips for JavaScript in V8” есть несколько интересных советов о работе с массивами.

Если вам эта статья показалась полезной, возможно вас заинтересует и “Object properties in JavaScript”.

9. Связаные темы

[1] Integers and shift operators in JavaScript
[2] JavaScript: sparse arrays vs. dense arrays

Comments