Creación de rango en JavaScript: sintaxis extraña

129

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?

Benjamin Gruenbaum
fuente
2
IMO 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.
fncomp
10
@fncomp Por favor, no use ya sea para realmente crear un rango. No solo es más lento que el enfoque directo, sino que no es tan fácil de entender. Es difícil entender la sintaxis (bueno, realmente la API y no la sintaxis) aquí, lo que hace que esta sea una pregunta interesante, pero terrible código de producción IMO.
Benjamin Gruenbaum
Sí, no sugiero que nadie lo use, pero pensó que aún era más fácil de leer, en relación con la versión literal del objeto.
fncomp
1
No estoy seguro de por qué alguien querría hacer esto. La cantidad de tiempo que lleva crear la matriz de esta manera podría haberse hecho de una manera un poco menos sexy pero mucho más rápida: jsperf.com/basic-vs-extreme
Eric Hodonsky

Respuestas:

263

Comprender este "truco" requiere comprender varias cosas:

  1. ¿Por qué no solo hacemos Array(5).map(...)
  2. Cómo Function.prototype.applymaneja los argumentos
  3. Cómo Arraymaneja múltiples argumentos
  4. Cómo Numbermaneja la función los argumentos
  5. Que Function.prototype.callhace

Son 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 lengthvariable mágica , pero en esencia, es un key => valuemapa 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.lengthy la cantidad de key=>valueasignaciones que tiene la matriz, que puede ser diferente de arr.length.

Expandir la matriz a través de arr.length no crea nuevas key=>valueasignaciones, 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.mapno 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.toUpperCasehabrí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 lenes un uint32 válido, y no cualquier número de valor)

Ahora puede ver por Array(5).map(...)qué no funcionaría: no definimos lenelementos en la matriz, no creamos las key => valueasignaciones, simplemente modificamos la lengthpropiedad.

Ahora que tenemos eso fuera del camino, veamos la segunda cosa mágica:

2. ¿Cómo Function.prototype.applyfunciona?

Lo que applyhace 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 applyfunciona simplemente registrando la argumentsvariable 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 => valueasignación puede no haber existido en la matriz a la que pasamos apply, pero ciertamente existe en la argumentsvariable. 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.applyse define. Principalmente cosas que no nos importan, pero aquí está la parte interesante:

  1. 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 forbucle simple sobre los lengthelementos, haciendo un listvalor correspondiente ( listes 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 argArrayen este caso es un objeto con una lengthpropiedad. Y ahora podemos ver por qué los valores no están definidos, pero las claves no lo están, en arguments: Creamos las key=>valueasignaciones.

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

3. Cómo Arraymaneja múltiples argumentos

¡Entonces! Vimos lo que sucede cuando pasas un lengthargumento 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.

La primera parte de la expresión.

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.

4. Cómo Numbertrata la entrada

Hacer Number(something)( sección 15.7.1 ) se convierte somethingen 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

calles applyel 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.calles solo una función, equivalente al callmétodo de cualquier otra función , y como tal, también tiene un callmétodo en sí mismo:

log.call === log.call.call; //true
log.call === Function.call; //true

Y que hace call? Acepta un thisArgconjunto 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}])

La parte posterior, o la .mapde todo

Aú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 thisargumento 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 forEachse 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.

En conclusión,

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]
Zirak
fuente
@Zirak Por favor, ayúdame a comprender lo siguiente ahaExclamationMark.apply(null, Array(2)); //2, true. ¿Por qué vuelve 2y truerespectivamente? ¿No estás pasando un solo argumento, es decir, Array(2)aquí?
Geek
44
@Geek Solo pasamos un argumento a apply, pero ese argumento se "salpica" en dos argumentos pasados ​​a la función. Puedes ver eso más fácilmente en los primeros applyejemplos. El primero console.logmuestra que, de hecho, recibimos dos argumentos (los dos elementos de la matriz), y el segundo console.logmuestra que la matriz tiene un key=>valuemapeo en la primera ranura (como se explica en la primera parte de la respuesta).
Zirak
44
Debido a (alguna) solicitud , ahora puede disfrutar de la versión de audio: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak
1
Tenga en cuenta que pasar un NodeList, que es un objeto host, a un método nativo como 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.
RobG
2
Una corrección: thissolo se establece Windowen modo no estricto.
ComFreek
21

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.

  • El thisvalor es nulllo que no importa para el constructor de Array ( thises el mismo thisque en el contexto de acuerdo con 15.3.4.3.2.a.
  • Luego new Arrayse llama pasar un objeto con una lengthpropiedad, lo que hace que ese objeto sea una matriz como todo lo que importa .applydebido a la siguiente cláusula en .apply:
    • Sea len el resultado de llamar al método interno [[Get]] de argArray con el argumento "length".
  • Como tal, .applyestá el paso de argumentos de 0 a .length, ya que llamar [[Get]]en { length: 5 }con los valores de 0 a 4 rendimientos undefineddel constructor array es llamado con cinco argumentos cuyo valor es undefined(obtener una propiedad no declarada de un objeto).
  • El constructor de matriz se llama con 0, 2 o más argumentos . La propiedad de longitud de la matriz recién construida se establece en el número de argumentos según la especificación y los valores en los mismos valores.
  • Por lo tanto, 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 undefinedy el segundo crea una matriz vacía de longitud 5. Específicamente, debido al .mapcomportamiento 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.mapllama a la función de devolución de llamada (en este caso Number.call) en cada elemento de la matriz y utiliza el thisvalor especificado (en este caso, establece el thisvalor en `Número).
  • El segundo parámetro de la devolución de llamada en el mapa (en este caso Number.call) es el índice, y el primero es el valor de este.
  • Esto significa que Numberse llama con thisas undefined(el valor de la matriz) y el índice como parámetro. Por lo tanto, es básicamente lo mismo que asignar cada uno undefineda su índice de matriz (ya que la llamada Numberrealiza 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.

Benjamin Gruenbaum
fuente
1
Para documentos: Especificación de cómo funciona el mapa: es5.github.io/#x15.4.4.19 , Mozilla tiene un script de muestra que funciona de acuerdo con esa especificación en developer.mozilla.org/en-US/docs/Web/JavaScript/ Referencia / ...
Patrick Evans
1
Pero, ¿por qué solo funciona Array.apply(null, { length: 2 })y no Array.apply(null, [2])lo que también llamaría al Arrayconstructor pasar 2como el valor de longitud? violín
Andreas
@Andreas 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 undefineddos 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.
Benjamin Gruenbaum
No he entendido cómo funciona en la primera ejecución ... Después de la segunda lectura tiene sentido. {length: 2}falsifica una matriz con dos elementos que el Arrayconstructor insertaría en la matriz recién creada. Como no hay una matriz real que acceda a los elementos no presentes, se obtiene lo undefinedque luego se inserta. Buen truco :)
Andreas
5

Como dijiste, la primera parte:

var arr = Array.apply(null, { length: 5 }); 

crea una matriz de 5 undefinedvalores.

La segunda parte está llamando a la mapfunción de la matriz que toma 2 argumentos y devuelve una nueva matriz del mismo tamaño.

El primer argumento que maptoma 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

  • a como el valor del elemento iterado actual
  • b como el índice del elemento iterado actual
  • c como toda la matriz original

El segundo argumento que maptoma se pasa a la función que pasa como primer argumento. Pero no sería a, b, ni c en caso de fooque 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 mappasa 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 bazen el ejemplo anterior. Number.callintentará analizar el índice; naturalmente, devolverá el mismo valor.

El segundo argumento que pasó a la mapfunción en su código en realidad no tiene un efecto en el resultado. Corrígeme si me equivoco, por favor.

Tal Z
fuente
1
Number.callno 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.
Bergi
0

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] === undefinedproduce 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

shex
fuente
1
Hay una gran diferencia entre [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.applyse utilizó lo impar .
Bergi