Перевод статьи 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