Перевод статьи Dr. Axel Rauschmayer‘а “Arrays in JavaScript”.
Статья посвящена особенностям реализации массивов в JavaScript. Как ни странно, они мало чем отличаются от обычных объектов.
1. В двух словах
Как вы должно быть знаете, объекты в JavaScript’е это хэши, состоящие из пар ключ-значение, где в качестве ключей используются строки. Массивы – те же объекты, лишь с парой особенностей:
- Индексы массива: если ключ является строковым представлением неотрицательного целого числа меньше некоего фиксированного значения, то он трактуется как индекс массива.
"length"
: значение этого свойства – неотрицательное целое, являющееся длиной массива. Длина массива определяется как числовое значение самого большого индекса плюс один.
То, что индексы массивов в JavaScript’е на самом деле являются строками, может вызвать недоумение, особенно если вы знакомы с другими языками. Конечно, JavaScript-движки внутри себя оптимизируют работу с массивами и используют числа в качестве индексов. Но в спецификации индексы описаны как строки и работать с ними приходится как со строками:
1 2 3 |
|
Так как 0
не является валидным идентификатором, использование точечной нотации (arr.0
) вызывает синтаксическую ошибку, из-за чего приходится использовать квадратные скобки. Оператор квадратные скобки преобразует свой операнд в строку, поэтому arr[0]
работает нормально, что и видно выше. Индекс элемента в массиве не может превышать 32 бита (примерно). Аналогично оператору квадратные скобки, оператор in
также преобразует свой первый операнд в строку, поэтому можно использовать числа для того, чтобы проверить наличие в массиве элемента с данным индексом:
1 2 |
|
Дальше будет более подробно описано то, как устроены массивы в 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 |
|
Подробнее про плотные и разреженные массивы можно прочитать в статье по ссылке [2].
3. Конструктор Array
Конструктор Array можно вызвать тремя способами:
new Array()
: создаёт новый пустой массив. Пустой литерал массива[]
является более короткой записью этой же операции.new Array(len)
: создаёт массив сlen
лакунами. В некоторых JavaScript-движках эта операция ведёт к выделению памяти для массива, что улучшает производительность в случае небольших массивов (для больших это не так). Но в большинстве случаев производительность не важна и стоит избегать ненужного усложнения, связанного с выделением памяти. Если это возможно, лучше создать массив с помощью литерала массива, сразу указав все элементы.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 |
|
Из условия (1) следует, что несмотря на то, что многие строки могут быть преобразованы в 32-битное целое без знака, только некоторые из них являются валидными индексами массива:
1 2 3 4 5 |
|
Только первая строка удовлетворяет условию (1) и является валидным индексом. Любые строки, не удовлетворяющие условиям выше, трактуются как обычные имена свойств:
1 2 3 |
|
5. length
Свойство length
массива является неотрицательным целым l
в интервале
0 ≤ l ≤ 2^32 − 1
(32 бита).
5.1 Отслеживание индексов
Когда в массив добавляются новые элементы, length
автоматически увеличивается:
1 2 3 4 |
|
5.2 Уменьшение length
Если длине массива присвоить значение l'
, которое меньше текущего значения l
, то из массива будут удалены элементы, индексы которых удовлетворяют условию l' ≤ i < l
:
1 2 3 4 5 |
|
5.3 Увеличение length
Если присвоить length
значение больше текущего, то в массиве появятся лакуны:
1 2 3 |
|
5.4 Допустимые значения length
Свойству length
можно присвоить произвольное значение, но преобразование этого значения в число к помощью функций ToUint32()
и Number()
обязаны давать одинаковый результат. Например:
1 2 3 4 5 6 |
|
Все значения, помеченные звёздочкой, являются допустимыми, остальные – нет:
1 2 3 4 5 6 |
|
Это можно легко проверить:
1 2 3 |
|
6. Экземпляры “класса” Array
Экземпляры “класса” Array являются обычными объектами, с единственным отличием: определение некоторых свойств происходит особым образом:
- Индексы массива: значение
length
увеличивается, если это необходимо. length
: бросается исключение, если значение является недопустимым; удаляются элементы массива, если новое значение меньше текущего.
Все остальные свойства ведут себя точно также, как у обычных объектов. Обратите внимание, что определение свойств происходит и при использовании оператора присваивания (вызывается внутренний метод [[Put]], который, в свою очередь, вызовет [[DefineOwnProperty]], если в цепочке прототипов не найдётся сеттера для этого свойства).
Вышесказанное означает, что невозможно создать “подкласс” “класса” Array, используя стандартный ECMAScript 5. Подробнее об этом можно прочитать в статье “Subtyping JavaScript built-ins”.
7. Выходя за пределы
Что произойдёт, если попытаться обратиться к элементу с индексом, превосходящим максимально допустимое значение? Индекс будет интерпретирован как обычное имя свойства объекта. Если попытаться использовать такой индекс для добавления значения в массив, новый элемент массива не будет создан. Ниже иллюстрация использования слишком большого индекса:
1 2 3 4 5 6 7 8 |
|
Если же попытаться добавить элемент в массив, уже имеющий максимальную длину, возникнет ошибка:
1 2 3 |
|
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