Me he encontrado con el siguiente código en la lista de correo es-discusion:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Esto produce
[0, 1, 2, 3, 4]
¿Por qué es este el resultado del código? ¿Que esta pasando aqui?
javascript
ecmascript-5
Benjamin Gruenbaum
fuente
fuente
Array.apply(null, Array(30)).map(Number.call, Number)
es más fácil de leer ya que evita fingir que un objeto simple es una matriz.Respuestas:
Comprender este "truco" requiere comprender varias cosas:
Array(5).map(...)
Function.prototype.apply
maneja los argumentosArray
maneja múltiples argumentosNumber
maneja la función los argumentosFunction.prototype.call
haceSon temas bastante avanzados en JavaScript, por lo que será más que bastante largo. Comenzaremos desde arriba. ¡Cinturón de seguridad!
1. ¿Por qué no solo
Array(5).map
?¿Qué es una matriz realmente? Un objeto regular, que contiene claves enteras, que se asignan a valores. Tiene otras características especiales, por ejemplo, la
length
variable mágica , pero en esencia, es unkey => value
mapa regular , como cualquier otro objeto. Juguemos un poco con las matrices, ¿de acuerdo?Llegamos a la diferencia inherente entre el número de elementos en la matriz
arr.length
y la cantidad dekey=>value
asignaciones que tiene la matriz, que puede ser diferente dearr.length
.Expandir la matriz a través de
arr.length
no crea nuevaskey=>value
asignaciones, por lo que no es que la matriz tenga valores indefinidos, no tiene estas claves . ¿Y qué sucede cuando intentas acceder a una propiedad inexistente? Se obtieneundefined
.Ahora podemos levantar un poco la cabeza y ver por qué funciones como
arr.map
no caminar sobre estas propiedades. Siarr[3]
simplemente no estuviera definido y existiera la clave, todas estas funciones de matriz simplemente lo revisarían como cualquier otro valor:Intencionalmente utilicé una llamada al método para probar aún más el punto de que la clave en sí nunca estuvo allí: la llamada
undefined.toUpperCase
habría provocado un error, pero no fue así. Para probar eso :Y ahora llegamos a mi punto: cómo
Array(N)
funcionan las cosas. La Sección 15.4.2.2 describe el proceso. Hay un montón de mumbo jumbo que no nos importan, pero si logras leer entre líneas (o puedes confiar en mí en este caso, pero no), básicamente se reduce a esto:(opera bajo el supuesto (que se verifica en la especificación real) que
len
es un uint32 válido, y no cualquier número de valor)Ahora puede ver por
Array(5).map(...)
qué no funcionaría: no definimoslen
elementos en la matriz, no creamos laskey => value
asignaciones, simplemente modificamos lalength
propiedad.Ahora que tenemos eso fuera del camino, veamos la segunda cosa mágica:
2. ¿Cómo
Function.prototype.apply
funciona?Lo que
apply
hace es básicamente tomar una matriz y desenrollarla como argumentos de una llamada de función. Eso significa que los siguientes son más o menos lo mismo:Ahora, podemos facilitar el proceso de ver cómo
apply
funciona simplemente registrando laarguments
variable especial:Es fácil probar mi reclamo en el penúltimo ejemplo:
(Sí, juego de palabras). La
key => value
asignación puede no haber existido en la matriz a la que pasamosapply
, pero ciertamente existe en laarguments
variable. Es la misma razón por la que funciona el último ejemplo: las claves no existen en el objeto que pasamos, pero sí existenarguments
.¿Porqué es eso? Veamos la Sección 15.3.4.3 , donde
Function.prototype.apply
se define. Principalmente cosas que no nos importan, pero aquí está la parte interesante:Que básicamente significa:
argArray.length
. La especificación luego procede a hacer unfor
bucle simple sobre loslength
elementos, haciendo unlist
valor correspondiente (list
es un vudú interno, pero es básicamente una matriz). En términos de código muy, muy suelto:Entonces, todo lo que necesitamos para imitar un
argArray
en este caso es un objeto con unalength
propiedad. Y ahora podemos ver por qué los valores no están definidos, pero las claves no lo están, enarguments
: Creamos laskey=>value
asignaciones.Uf, así que esto podría no haber sido más corto que la parte anterior. Pero habrá pastel cuando terminemos, ¡así que sé paciente! Sin embargo, después de la siguiente sección (que será breve, lo prometo) podemos comenzar a diseccionar la expresión. En caso de que lo haya olvidado, la pregunta era cómo funciona lo siguiente:
3. Cómo
Array
maneja múltiples argumentos¡Entonces! Vimos lo que sucede cuando pasas un
length
argumento aArray
, pero en la expresión, pasamos varias cosas como argumentos (una matriz de 5undefined
, para ser exactos). La Sección 15.4.2.1 nos dice qué hacer. El último párrafo es todo lo que nos importa, y está redactado de manera muy extraña, pero se reduce a:Tada! Obtenemos una matriz de varios valores indefinidos y devolvemos una matriz de estos valores indefinidos.
La primera parte de la expresión.
Finalmente, podemos descifrar lo siguiente:
Vimos que devuelve una matriz que contiene 5 valores indefinidos, con claves todas en existencia.
Ahora, a la segunda parte de la expresión:
Esta será la parte más fácil y no complicada, ya que no depende tanto de hacks oscuros.
4. Cómo
Number
trata la entradaHacer
Number(something)
( sección 15.7.1 ) se conviertesomething
en un número, y eso es todo. Cómo lo hace es un poco complicado, especialmente en los casos de cadenas, pero la operación se define en la sección 9.3 en caso de que le interese.5. Juegos de
Function.prototype.call
call
esapply
el hermano de, definido en la sección 15.3.4.4 . En lugar de tomar una serie de argumentos, solo toma los argumentos que recibió y los pasa hacia adelante.Las cosas se ponen interesantes cuando se encadenan más de uno
call
, aumentan el extraño hasta 11:Esto es bastante valioso hasta que comprenda lo que está sucediendo.
log.call
es solo una función, equivalente alcall
método de cualquier otra función , y como tal, también tiene uncall
método en sí mismo:Y que hace
call
? Acepta unthisArg
conjunto de argumentos y llama a su función padre. Podemos definirlo a través deapply
(nuevamente, código muy suelto, no funcionará):Hagamos un seguimiento de cómo sucede esto:
La parte posterior, o la
.map
de todoAún no ha terminado. Veamos qué sucede cuando proporciona una función a la mayoría de los métodos de matriz:
Si no proporcionamos un
this
argumento nosotros mismos, lo predeterminado eswindow
. Tome nota del orden en el que se proporcionan los argumentos para nuestra devolución de llamada, y vamos a volver a dividirlo en 11:Whoa whoa whoa ... retrocedamos un poco. ¿Que está pasando aqui? Podemos ver en la sección 15.4.4.18 , donde
forEach
se define, sucede lo siguiente:Entonces, obtenemos esto:
Ahora podemos ver cómo
.map(Number.call, Number)
funciona:Lo que devuelve la transformación de
i
, el índice actual, a un número.En conclusión,
La expresion
Funciona en dos partes:
La primera parte crea una matriz de 5 elementos indefinidos. El segundo pasa sobre esa matriz y toma sus índices, lo que resulta en una matriz de índices de elementos:
fuente
ahaExclamationMark.apply(null, Array(2)); //2, true
. ¿Por qué vuelve2
ytrue
respectivamente? ¿No estás pasando un solo argumento, es decir,Array(2)
aquí?apply
, pero ese argumento se "salpica" en dos argumentos pasados a la función. Puedes ver eso más fácilmente en los primerosapply
ejemplos. El primeroconsole.log
muestra que, de hecho, recibimos dos argumentos (los dos elementos de la matriz), y el segundoconsole.log
muestra que la matriz tiene unkey=>value
mapeo en la primera ranura (como se explica en la primera parte de la respuesta).log.apply(null, document.getElementsByTagName('script'));
no es necesario para funcionar y no funciona en algunos navegadores, y[].slice.call(NodeList)
convertir un NodeList en una matriz tampoco funcionará en ellos.this
solo se estableceWindow
en modo no estricto.Exención de responsabilidad : Esta es una descripción muy formales del código de arriba - así es como yo sé cómo explicarlo. Para una respuesta más simple, consulte la excelente respuesta de Zirak arriba. Esta es una especificación más profunda en su cara y menos "ajá".
Varias cosas están sucediendo aquí. Vamos a dividirlo un poco.
En la primera línea, el constructor de matriz se llama como una función con
Function.prototype.apply
.this
valor esnull
lo que no importa para el constructor de Array (this
es el mismothis
que en el contexto de acuerdo con 15.3.4.3.2.a.new Array
se llama pasar un objeto con unalength
propiedad, lo que hace que ese objeto sea una matriz como todo lo que importa.apply
debido a la siguiente cláusula en.apply
:.apply
está el paso de argumentos de 0 a.length
, ya que llamar[[Get]]
en{ length: 5 }
con los valores de 0 a 4 rendimientosundefined
del constructor array es llamado con cinco argumentos cuyo valor esundefined
(obtener una propiedad no declarada de un objeto).var arr = Array.apply(null, { length: 5 });
crea una lista de cinco valores indefinidos.Nota : Observe la diferencia aquí entre
Array.apply(0,{length: 5})
yArray(5)
, el primero crea cinco veces el tipo de valor primitivoundefined
y el segundo crea una matriz vacía de longitud 5. Específicamente, debido al.map
comportamiento de (8.b) y específicamente[[HasProperty]
.Entonces, el código anterior en una especificación compatible es el mismo que:
Ahora a la segunda parte.
Array.prototype.map
llama a la función de devolución de llamada (en este casoNumber.call
) en cada elemento de la matriz y utiliza elthis
valor especificado (en este caso, establece elthis
valor en `Número).Number.call
) es el índice, y el primero es el valor de este.Number
se llama conthis
asundefined
(el valor de la matriz) y el índice como parámetro. Por lo tanto, es básicamente lo mismo que asignar cada unoundefined
a su índice de matriz (ya que la llamadaNumber
realiza la conversión de tipo, en este caso de número a número sin cambiar el índice).Por lo tanto, el código anterior toma los cinco valores indefinidos y asigna cada uno a su índice en la matriz.
Es por eso que obtenemos el resultado de nuestro código.
fuente
Array.apply(null, { length: 2 })
y noArray.apply(null, [2])
lo que también llamaría alArray
constructor pasar2
como el valor de longitud? violínArray.apply(null,[2])
es como elArray(2)
que crea una matriz vacía de longitud 2 y no una matriz que contiene el valor primitivoundefined
dos veces. Vea mi edición más reciente en la nota después de la primera parte, avíseme si está lo suficientemente clara y si no, aclararé sobre eso.{length: 2}
falsifica una matriz con dos elementos que elArray
constructor insertaría en la matriz recién creada. Como no hay una matriz real que acceda a los elementos no presentes, se obtiene loundefined
que luego se inserta. Buen truco :)Como dijiste, la primera parte:
crea una matriz de 5
undefined
valores.La segunda parte está llamando a la
map
función de la matriz que toma 2 argumentos y devuelve una nueva matriz del mismo tamaño.El primer argumento que
map
toma es en realidad una función para aplicar en cada elemento de la matriz, se espera que sea una función que tome 3 argumentos y devuelva un valor. Por ejemplo:si pasamos la función foo como primer argumento, se llamará para cada elemento con
El segundo argumento que
map
toma se pasa a la función que pasa como primer argumento. Pero no sería a, b, ni c en caso defoo
que lo fuerathis
.Dos ejemplos:
y otro solo para aclararlo:
¿Y qué hay de Number.call?
Number.call
es una función que toma 2 argumentos e intenta analizar el segundo argumento a un número (no estoy seguro de qué hace con el primer argumento).Como el segundo argumento que
map
pasa es el índice, el valor que se colocará en la nueva matriz en ese índice es igual al índice. Al igual que la funciónbaz
en el ejemplo anterior.Number.call
intentará analizar el índice; naturalmente, devolverá el mismo valor.El segundo argumento que pasó a la
map
función en su código en realidad no tiene un efecto en el resultado. Corrígeme si me equivoco, por favor.fuente
Number.call
no es una función especial que analiza argumentos a números. Es simplemente=== Function.prototype.call
. Solo el segundo argumento, la función que se pasa comothis
-valor acall
, es relevante -.map(eval.call, Number)
,.map(String.call, Number)
y.map(Function.prototype.call, Number)
todos son equivalentes.Una matriz es simplemente un objeto que comprende el campo 'longitud' y algunos métodos (por ejemplo, push). Entonces arr in
var arr = { length: 5}
es básicamente lo mismo que una matriz donde los campos 0..4 tienen el valor predeterminado que no está definido (es decir,arr[0] === undefined
produce verdadero).En cuanto a la segunda parte, map, como su nombre lo indica, asigna de una matriz a una nueva. Lo hace atravesando la matriz original e invocando la función de mapeo en cada elemento.
Todo lo que queda es convencerte de que el resultado de la función de mapeo es el índice. El truco consiste en utilizar el método llamado 'call' (*) que invoca una función con la pequeña excepción de que el primer parámetro está configurado para ser el contexto 'this', y el segundo se convierte en el primer parámetro (y así sucesivamente). Casualmente, cuando se invoca la función de mapeo, el segundo parámetro es el índice.
Por último, pero no menos importante, el método que se invoca es el Número "Clase", y como sabemos en JS, una "Clase" es simplemente una función, y este (Número) espera que el primer parámetro sea el valor.
(*) encontrado en el prototipo de Function (y Number es una función).
MASHAL
fuente
[undefined, undefined, undefined, …]
ynew Array(n)
o{length: n}
: los últimos son escasos , es decir, no tienen elementos. Esto es muy relevante paramap
, y es por eso queArray.apply
se utilizó lo impar .