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?
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?
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!
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 un key => value
mapa regular , como cualquier otro objeto. Juguemos un poco con las matrices, ¿de acuerdo?
var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined
//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']
Llegamos a la diferencia inherente entre el número de elementos en la matriz arr.length
y la cantidad de key=>value
asignaciones que tiene la matriz, que puede ser diferente de arr.length
.
Expandir la matriz a través de arr.length
no crea nuevas key=>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 obtiene undefined
.
Ahora podemos levantar un poco la cabeza y ver por qué funciones como arr.map
no caminar sobre estas propiedades. Si arr[3]
simplemente no estuviera definido y existiera la clave, todas estas funciones de matriz simplemente lo revisarían como cualquier otro valor:
//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';
arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']
arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]
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 :
arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined
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:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(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 definimos len
elementos en la matriz, no creamos las key => value
asignaciones, simplemente modificamos la length
propiedad.
Ahora que tenemos eso fuera del camino, veamos la segunda cosa mágica:
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:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Ahora, podemos facilitar el proceso de ver cómo apply
funciona simplemente registrando la arguments
variable especial:
function log () {
console.log(arguments);
}
log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
//["mary", "had", "a", "little", "lamb"]
//arguments is a pseudo-array itself, so we can use it as well
(function () {
log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
//["mary", "had", "a", "little", "lamb"]
//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
//[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]
//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!
log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]
Es fácil probar mi reclamo en el penúltimo ejemplo:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(Sí, juego de palabras). La key => value
asignación puede no haber existido en la matriz a la que pasamos apply
, pero ciertamente existe en la arguments
variable. Es la misma razón por la que funciona el último ejemplo: las claves no existen en el objeto que pasamos, pero sí existen arguments
.
¿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:
- Sea len el resultado de llamar al método interno [[Get]] de argArray con el argumento "length".
Que básicamente significa: argArray.length
. La especificación luego procede a hacer un for
bucle simple sobre los length
elementos, haciendo un list
valor correspondiente ( list
es un vudú interno, pero es básicamente una matriz). En términos de código muy, muy suelto:
Function.prototype.apply = function (thisArg, argArray) {
var len = argArray.length,
argList = [];
for (var i = 0; i < len; i += 1) {
argList[i] = argArray[i];
}
//yeah...
superMagicalFunctionInvocation(this, thisArg, argList);
};
Entonces, todo lo que necesitamos para imitar un argArray
en este caso es un objeto con una length
propiedad. Y ahora podemos ver por qué los valores no están definidos, pero las claves no lo están, en arguments
: Creamos las key=>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:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Array
maneja múltiples argumentos¡Entonces! Vimos lo que sucede cuando pasas un length
argumento a Array
, pero en la expresión, pasamos varias cosas como argumentos (una matriz de 5 undefined
, 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:
function Array () {
var ret = [];
ret.length = arguments.length;
for (var i = 0; i < arguments.length; i += 1) {
ret[i] = arguments[i];
}
return ret;
}
Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]
Tada! Obtenemos una matriz de varios valores indefinidos y devolvemos una matriz de estos valores indefinidos.
Finalmente, podemos descifrar lo siguiente:
Array.apply(null, { length: 5 })
Vimos que devuelve una matriz que contiene 5 valores indefinidos, con claves todas en existencia.
Ahora, a la segunda parte de la expresión:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Esta será la parte más fácil y no complicada, ya que no depende tanto de hacks oscuros.
Number
trata la entradaHacer Number(something)
( sección 15.7.1 ) se convierte something
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.
Function.prototype.call
call
es apply
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:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Esto es bastante valioso hasta que comprenda lo que está sucediendo. log.call
es solo una función, equivalente al call
método de cualquier otra función , y como tal, también tiene un call
método en sí mismo:
log.call === log.call.call; //true
log.call === Function.call; //true
Y que hace call
? Acepta un thisArg
conjunto de argumentos y llama a su función padre. Podemos definirlo a través de apply
(nuevamente, código muy suelto, no funcionará):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Hagamos un seguimiento de cómo sucede esto:
log.call.call(log, {a:4}, {a:5});
this = log.call
thisArg = log
args = [{a:4}, {a:5}]
log.call.apply(log, [{a:4}, {a:5}])
log.call({a:4}, {a:5})
this = log
thisArg = {a:4}
args = [{a:5}]
log.apply({a:4}, [{a:5}])
.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:
function log () {
console.log(this, arguments);
}
var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^ ^-----------------------^
// this arguments
Si no proporcionamos un this
argumento nosotros mismos, lo predeterminado es window
. 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:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
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:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Entonces, obtenemos esto:
log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);
Ahora podemos ver cómo .map(Number.call, Number)
funciona:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Lo que devuelve la transformación de i
, el índice actual, a un número.
La expresion
Array.apply(null, { length: 5 }).map(Number.call, Number);
Funciona en dos partes:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
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:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
. ¿Por qué vuelve 2
y true
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 primeros apply
ejemplos. El primero console.log
muestra que, de hecho, recibimos dos argumentos (los dos elementos de la matriz), y el segundo console.log
muestra que la matriz tiene un key=>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 establece Window
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.
var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
En la primera línea, el constructor de matriz se llama como una función con Function.prototype.apply
.
this
valor es null
lo que no importa para el constructor de Array ( this
es el mismo this
que en el contexto de acuerdo con 15.3.4.3.2.a.new Array
se llama pasar un objeto con una length
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 rendimientos undefined
del constructor array es llamado con cinco argumentos cuyo valor es undefined
(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})
y Array(5)
, el primero crea cinco veces el tipo de valor primitivo undefined
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:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Ahora a la segunda parte.
Array.prototype.map
llama a la función de devolución de llamada (en este caso Number.call
) en cada elemento de la matriz y utiliza el this
valor especificado (en este caso, establece el this
valor en `Número).Number.call
) es el índice, y el primero es el valor de este.Number
se llama con this
as undefined
(el valor de la matriz) y el índice como parámetro. Por lo tanto, es básicamente lo mismo que asignar cada uno undefined
a su índice de matriz (ya que la llamada Number
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.
Array.apply(null,[2])
es como el Array(2)
que crea una matriz vacía de longitud 2 y no una matriz que contiene el valor primitivo undefined
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 el Array
constructor insertaría en la matriz recién creada. Como no hay una matriz real que acceda a los elementos no presentes, se obtiene lo undefined
que luego se inserta. Buen truco :)
Como dijiste, la primera parte:
var arr = Array.apply(null, { length: 5 });
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:
function foo(a,b,c){
...
return ...
}
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 de foo
que lo fuera this
.
Dos ejemplos:
function bar(a,b,c){
return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]
function baz(a,b,c){
return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]
y otro solo para aclararlo:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
¿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ón baz
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.
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 como this
-valor a call
, 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
[undefined, undefined, undefined, …]
y new Array(n)
o {length: n}
: los últimos son escasos , es decir, no tienen elementos. Esto es muy relevante para map
, y es por eso que Array.apply
se utilizó lo impar .
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.