¿Son escasas las matrices de Javascript?

97

Es decir, si uso la hora actual como índice en la matriz:

array[Date.getTime()] = value;

¿El intérprete instanciará todos los elementos desde 0 hasta ahora? ¿Los diferentes navegadores lo hacen de manera diferente?

Recuerdo que solía haber un error en el kernel de AIX , que creaba pseudo-ttys a pedido, pero si lo hiciera, di "echo> / dev / pty10000000000", crearía / dev / pty0, / dev / pty1, .... y luego caer muerto. Fue divertido en las ferias comerciales, pero no quiero que esto les pase a mis clientes.

Baya
fuente
1
Una posible desventaja de hacer esto es la dificultad de depurar en Firebug. una declaración de registro en la matriz solo enumerará los primeros 1000 elementos de la matriz, que serán todos "indefinidos". Además, array.length le dirá que su matriz tiene n elementos, aunque n-1 son simplemente valores indefinidos "fantasma".
Michael Butler
La depuración ahora está bien en Chrome: aquí hay un ejemplo de salida de consola: [vacío × 9564, Objeto, vacío × 105, Objeto, vacío × 10, Objeto, vacío × 12, Objeto, vacío × 9, Objeto, vacío × 21, Objeto, vacío × 9, Objeto]
jsalvata

Respuestas:

40

La forma exacta en que se implementan las matrices de JavaScript difiere de un navegador a otro, pero generalmente recurren a una implementación escasa, probablemente la misma que se usa para el acceso a las propiedades de los objetos regulares, si usar una matriz real sería ineficiente.

Tendrá que pedirle a alguien con más conocimiento sobre implementaciones específicas que responda qué provoca exactamente el cambio de denso a escaso, pero su ejemplo debería ser perfectamente seguro. Si desea obtener una matriz densa, debe llamar al constructor con un argumento de longitud explícito y esperar que realmente obtenga uno.

Consulte esta respuesta para obtener una descripción más detallada de olliej.

Christoph
fuente
1
No creo que obtengas una matriz densa si dices algo como foo = new Array(10000). Sin embargo, esto se supone que el trabajo: foo = Array.apply(null, {length: 10});.
doubleOrt
70

Sí lo son. En realidad, son tablas hash internamente, por lo que puede usar no solo números enteros grandes, sino también cadenas, flotantes u otros objetos. Todas las claves se convierten en cadenas toString()antes de agregarlas al hash. Puede confirmar esto con algún código de prueba:

<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>

Muestra:

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

Observe cómo utilicé la for...insintaxis, que solo le brinda los índices que realmente están definidos. Si usa el for (var i = 0; i < array.length; ++i)estilo de iteración más común , obviamente tendrá problemas con índices de matriz no estándar.

John Kugelman
fuente
9
la mayoría de las implementaciones de JS almacenan propiedades indexadas numéricamente en una matriz real si es posible; Sin embargo, eso es magia detrás de escena: desde el punto de vista del lenguaje, las matrices son objetos regulares con una lengthpropiedad mágica
Christoph
7
@John: lengthsolo es invisible en los for..inbucles porque tiene la DontEnumbandera establecida; en ES5, se llama al atributo de propiedad enumerabley se puede establecer explícitamente a través deObject.defineProperty()
Christoph
14
Todas las claves de objeto en JavaScript son siempre String; cualquier otra cosa que ponga en el subíndice obtiene toString()-ed. Combine esto con la imprecisión entera de un Número grande y significa que si establece a[9999999999999999]=1, a[10000000000000000]será 1 (y muchos más comportamientos sorprendentes). El uso de números no enteros como claves es muy imprudente, y los objetos arbitrarios son correctos.
Salida el
71
Entonces, solo usarás cadenas como claves de objeto, ni más ni menos. Cadena será el tipo que utilizará y el tipo de clave será Cadena. Integer no usarás, ni usarás no enteros, excepto que luego procedas a lanzar a String. Los objetos arbitrarios son correctos.
Crescent Fresh
8
Los índices de matriz deben ser números enteros. array [3.14] = pi funciona porque Array es inherente a Object. Ejemplo: var x = []; x [.1] = 5; Entonces x todavía tiene una longitud de 0.
Mike Blandford
10

Puede evitar el problema utilizando una sintaxis de JavaScript diseñada para este tipo de cosas. Puede tratarlo como un diccionario, pero la sintaxis "para ... en ..." le permitirá tomarlos todos.

var sparse = {}; // not []
sparse["whatever"] = "something";
John Fisher
fuente
7

Los objetos de Javascript son escasos y las matrices son solo objetos especializados con una propiedad de longitud mantenida automáticamente (que en realidad es uno más grande que el índice más grande, no el número de elementos definidos) y algunos métodos adicionales. Estás a salvo de cualquier manera; use una matriz si necesita sus características adicionales y un objeto en caso contrario.

Sólo en el amor
fuente
4
eso es desde el punto de vista del lenguaje; las implementaciones usan matrices reales para almacenar propiedades numéricas densas
Christoph
6

La respuesta, como suele ser cierto con JavaScript, es "es un poco más extraño ..."

El uso de memoria no está definido y se permite que cualquier implementación sea estúpida. En teoría, const a = []; a[1000000]=0;podría quemar megabytes de memoria, como podría const a = [];. En la práctica, incluso Microsoft evita esas implementaciones.

Justin Love señala que el atributo de longitud es el conjunto de índice más alto . PERO solo se actualiza si el índice es un número entero.

Entonces, la matriz es escasa. PERO funciones integradas como reduce (), Math.max () y "para ... de" recorrerán todo el rango de posibles índices enteros desde 0 hasta la longitud, visitando muchos que devuelven 'indefinido'. PERO los bucles 'for ... in' pueden hacer lo que espera, visitando solo las claves definidas.

Aquí tienes un ejemplo con 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]}`);

dando:

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

Pero. Hay más casos de esquina con matrices aún no mencionadas.

Charles Merriam
fuente
2

La escasez (o densidad) se puede confirmar empíricamente para NodeJS con el process.memoryUsage () no estándar .

A veces, el nodo es lo suficientemente inteligente como para mantener la matriz escasa:

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

A veces, el nodo elige hacerlo denso (este comportamiento bien podría optimizarse en el futuro):

> 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

Luego, escaso de nuevo:

> 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

Entonces, tal vez usar una matriz densa para tener una idea del error original del kernel de AIX podría necesitar ser forzado con un rango similar :

> 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

Porque ¿por qué no hacerlo caer?

> 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
pzrq
fuente
1
Bien, y estoy un poco sorprendido de que mi pregunta de diez años siga siendo relevante.
Berry
1

Pueden serlo, pero no siempre tienen que serlo, y pueden rendir mejor cuando no lo son.

Aquí hay una discusión sobre cómo probar la escasez de índices en una instancia de matriz: https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/

Este código ganador de golf (menos caracteres) es:

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

Básicamente, recorre la matriz para las entradas indexadas mientras se disminuye el valor de longitud y se devuelve el !!booleano reforzado del resultado numérico falso / verdadero (si el acumulador se reduce hasta cero, el índice está completamente poblado y no escaso). Las advertencias de Charles Merriam anteriores también deben tenerse en cuenta y este código no las aborda, pero se aplican a las entradas de cadenas hash que pueden suceder al asignar elementos con arr[var]= (something)var no era un entero.

Una razón para preocuparse por la escasez de índices son sus efectos en el rendimiento, que pueden diferir entre los motores de secuencia de comandos. Aquí hay una gran discusión sobre la creación / inicialización de matrices: ¿Cuál es la diferencia entre "Array ()" y "[]" al declarar un JavaScript ¿formación?

Una respuesta reciente a esa publicación tiene un enlace a esta inmersión profunda en cómo V8 intenta optimizar las matrices etiquetándolas para evitar (volver a) probar características como la escasez: https://v8.dev/blog/elements-kinds . La publicación del blog es del 17 de septiembre y el material está sujeto a algunos cambios, pero el desglose de las implicaciones para el desarrollo diario es útil y claro.

dkloke
fuente