Являются ли массивы Javascript редкими?

То есть, если я использую текущее время в качестве индекса в массиве:

array[Date.getTime()] = value;

будет ли интерпретатор создавать экземпляры всех элементов с 0 по настоящее время? Разные браузеры делают это по-разному?

Я помню, что раньше в ядре AIX была ошибка, которая создавала псевдо-ttys по запросу, но если бы вы, скажем, "echo > /dev/pty10000000000", это создавало /dev/pty0, /dev/pty1, .... а затем упасть замертво. На выставках было весело, но я не хочу, чтобы это случилось с моими клиентами.

4 ответа

Решение

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

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

Смотрите этот ответ для более подробного описания olliej.

Да. Они на самом деле являются внутренними хеш-таблицами, поэтому вы можете использовать не только большие целые числа, но также строки, числа с плавающей точкой или другие объекты. Все ключи преобразуются в строки через toString() перед добавлением в хеш. Вы можете подтвердить это с помощью некоторого тестового кода:

<script>
  var array = [];
  array[0] = "zero";
  array[new Date().getTime()] = "now";
  array[3.14] = "pi";

  for (var i in array) {
      alert("array["+i+"] = " + array[i] + ", typeof("+i+") == " + typeof(i));
  }
</script>

Вывод:

array[0] = zero, typeof(0) == string
array[1254503972355] = now, typeof(1254503972355) == string
array[3.14] = pi, typeof(3.14) == string

Обратите внимание, как я использовал for...in синтаксис, который дает только те индексы, которые на самом деле определены. Если вы используете более распространенный for (var i = 0; i < array.length; ++i) стиль итерации, то у вас, очевидно, будут проблемы с нестандартными индексами массивов.

Вы можете избежать этой проблемы, используя синтаксис JavaScript, разработанный для такого рода вещей. Вы можете рассматривать его как словарь, но синтаксис "for ... in ... " позволит вам захватить их все.

var sparse = {}; // not []
sparse["whatever"] = "something";

Объекты Javascript являются разреженными, а массивы - это просто специализированные объекты с автоматически поддерживаемым свойством длины (которое на самом деле больше, чем самый большой индекс, а не число определенных элементов) и некоторыми дополнительными методами. Вы в безопасности в любом случае; используйте массив, если вам нужны его дополнительные функции, и объект в противном случае.

Редкость (или плотность) для NodeJS можно подтвердить эмпирически с помощью нестандартного метода process.memoryUsage().

Иногда node достаточно умен, чтобы сохранить разреженный массив:

Welcome to Node.js v12.15.0.
Type ".help" for more information.
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 3.07 MB
undefined
> array = []
[]
> array[2**24] = 2**24
16777216
> array
[ <16777216 empty items>, 16777216 ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 2.8 MB
undefined

Иногда узел предпочитает сделать его плотным (это поведение вполне может быть оптимизировано в будущем):

> otherArray = Array(2**24)
[ <16777216 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.57 MB
undefined

Затем снова разреженный:

> yetAnotherArray = Array(2**32-1)
[ <4294967295 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.68 MB
undefined

Поэтому, возможно, использование плотного массива, чтобы почувствовать исходную ошибку ядра AIX, может потребоваться принудительно с подобным диапазоном:

> denseArray = [...Array(2**24).keys()]
[
   0,  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, 33, 34, 35,
  36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
  48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
  72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
  84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
  96, 97, 98, 99,
  ... 16777116 more items
]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`);
The script is using approximately 819.94 MB
undefined

Почему бы не заставить его упасть?

> tooDenseArray = [...Array(2**32-1).keys()]

<--- Last few GCs --->

[60109:0x1028ca000]   171407 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 
[60109:0x1028ca000]   171420 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 
[60109:0x1028ca000]   171434 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms  (average mu = 0.968, current mu = 0.832) allocation failure 


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x100931399]
    1: StubFrame [pc: 0x1008ee227]
    2: StubFrame [pc: 0x100996051]
Security context: 0x1043830808a1 <JSObject>
    3: /* anonymous */ [0x1043830b6919] [repl:1] [bytecode=0x1043830b6841 offset=28](this=0x104306fc2261 <JSGlobal Object>)
    4: InternalFrame [pc: 0x1008aefdd]
    5: EntryFrame [pc: 0x1008aedb8]
    6: builtin exit frame: runInThisContext(this=0x104387b8cac1 <ContextifyScript map = 0x1043...

FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory

Writing Node.js report to file: report.20200220.220620.60109.0.001.json
Node.js report completed
 1: 0x10007f4b9 node::Abort() [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 2: 0x10007f63d node::OnFatalError(char const*, char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 3: 0x100176a27 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 4: 0x1001769c3 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 5: 0x1002fab75 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 6: 0x1005f3e9b v8::internal::Runtime_FatalProcessOutOfMemoryInvalidArrayLength(int, unsigned long*, v8::internal::Isolate*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 7: 0x100931399 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
 8: 0x1008ee227 Builtins_IterableToList [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
Abort trap: 6

Ответ, как это обычно бывает с JavaScript, - "это немного сложнее...."

Использование памяти не определено, и любая реализация может быть глупой. Теоретически,const a = []; a[1000000]=0; мог сжечь мегабайты памяти, как мог const a = [];. На практике даже Microsoft избегает таких реализаций.

Justin Love указывает, что атрибут длины - это самый высокий индексный набор. НО он обновляется только в том случае, если индекс является целым числом.

Итак, массив разреженный. НО встроенные функции, такие как reduce(), Math.max() и "for ... of", будут проходить через весь диапазон возможных целочисленных индексов от 0 до длины, посещая многие из них, которые возвращают undefined. НО циклы for... in могут работать так, как вы ожидаете, посещая только определенные ключи.

Вот пример использования Node.js:

"use strict";
const print = console.log;

let a = [0, 10];
// a[2] and a[3] skipped
a[4] = 40;
a[5] = undefined;  // which counts towards setting the length
a[31.4] = 'ten pi';  // doesn't count towards setting the length
a['pi'] = 3.14;
print(`a.length= :${a.length}:, a = :${a}:`);
print(`Math.max(...a) = :${Math.max(a)}: because of 'undefined values'`);
for (let v of a) print(`v of a; v=:${v}:`);
for (let i in a) print(`i in a; i=:${i}: a[i]=${a[i]}`);

давая:

a.length= :6:, a = :0,10,,,40,:
Math.max(...a) = :NaN: because of 'undefined values'
v of a; v=:0:
v of a; v=:10:
v of a; v=:undefined:
v of a; v=:undefined:
v of a; v=:40:
v of a; v=:undefined:
i in a; i=:0: a[i]=0
i in a; i=:1: a[i]=10
i in a; i=:4: a[i]=40
i in a; i=:5: a[i]=undefined
i in a; i=:31.4: a[i]=ten pi
i in a; i=:pi: a[i]=3.14

Но. Есть и другие случаи, когда массивы еще не упомянуты.

Они могут быть, но не всегда, и они могут работать лучше, когда это не так.

Вот обсуждение того, как проверить разреженность индекса в экземпляре массива: https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/

Этот код победителя гольфа (наименьшее количество символов):

let isSparse = a => !!a.reduce(x=>x-1,a.length)

В основном ходят по массиву для индексированных записей, уменьшая значение длины и возвращая усиленный !!логическое значение ложного / правдивого числового результата (если аккумулятор полностью уменьшается до нуля, индекс заполняется полностью, а не разреженным). Charles Merriam следует учитывать приведенные выше предостережения Charles Merriam, и этот код не обращается к ним, но они применяются к хешированным строковым записям, которые могут произойти при назначении элементов с помощьюarr[var]= (something) где var не было целым числом.

Причина, по которой нужно заботиться о разреженности индекса, заключается в его влиянии на производительность, которое может различаться в зависимости от движка сценария. Здесь идет большая дискуссия о создании / инициализации массива: в чем разница между "Array()" и "[]", в то время как объявление массива JavaScript?

В недавнем ответе на этот пост есть ссылка на это подробное описание того, как V8 пытается оптимизировать массивы, помечая их, чтобы избежать (повторного) тестирования на такие характеристики, как разреженность: https://v8.dev/blog/elements-kinds. Сообщение в блоге от сентября 2017 года, и в него могут быть внесены некоторые изменения, но разбивка по значениям для повседневной разработки полезна и понятна.

Другие вопросы по тегам