Crear rango en JavaScript-sintaxis extraña


Me he encontrado con el siguiente código en la lista de correo es-discuss:

Array.apply(null, { length: 5 }).map(Number.call, Number);

Esto produce

[0, 1, 2, 3, 4]

¿Por qué este es el resultado del código? ¿Qué está pasando aquí?

Author: Peter Mortensen, 2013-09-22

4 answers

Entender este "hackeo" requiere entender varias cosas: {[89]]}

  1. Por qué no solo hacemos {[24]]}
  2. Cómo maneja Function.prototype.apply los argumentos
  3. Cómo Array maneja múltiples argumentos
  4. Cómo maneja la función Number los argumentos
  5. Qué hace Function.prototype.call

Son temas bastante avanzados en javascript, por lo que será más que largo. Empezaremos desde el principio. ¡Abróchate el cinturón!

1. ¿Por qué no solo Array(5).map?

¿Qué es una matriz, ¿en serio? Un objeto regular, que contiene claves enteras, que se asignan a valores. Tiene otras características especiales, por ejemplo la variable mágica length, pero en su núcleo, es un mapa regular key => value, al igual que cualquier otro objeto. Juguemos un poco con arreglos, ¿sí?

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 el número de asignaciones key=>value que tiene la matriz, que puede ser diferente de arr.length.

Expandiendo la matriz a través de arr.length no crea ninguna nueva asignación key=>value, por lo que no es que el array tenga valores indefinidos, sino que no tiene estas claves. ¿Y qué sucede cuando intentas acceder a una propiedad inexistente? Se obtiene undefined.

Ahora podemos levantar la cabeza un poco, y ver por qué funciones como arr.map no caminan sobre estas propiedades. Si arr[3] fuera simplemente indefinido, y la clave existiera, todas estas funciones de matriz simplemente pasarían por encima de ella 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"]

I intencionalmente se usó una llamada al método para probar aún más el punto de que la clave en sí nunca estuvo allí: Llamar undefined.toUpperCase habría generado un error, pero no lo hizo. Para probar que :

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) hace las cosas. La sección 15.4.2.2 describe el proceso. Hay un montón de tonterías que no nos importan, pero si te las arreglas para 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 la suposición (que se comprueba en la especificación real) de que len es un uint32 válido, y no cualquier número de valor)

Así que ahora puedes ver por qué hacer Array(5).map(...) no funcionaría - no definimos len elementos en la matriz, no creamos las asignaciones key => value, simplemente alteramos la propiedad length.

Ahora que tenemos eso fuera del camino, veamos la segunda cosa mágica: {[89]]}

2. Cómo funciona Function.prototype.apply

Qué hace apply es básicamente tomar una matriz y desenrollarla como argumentos de una llamada a función. Eso significa que los siguientes son más o menos los mismos:

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 funciona apply simplemente registrando la variable especial arguments:

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 afirmación en el penúltimo ejemplo: {[89]]}

function ahaExclamationMark () {
    console.log(arguments.length);
    console.log(arguments.hasOwnProperty(0));
}

ahaExclamationMark.apply(null, Array(2)); //2, true

(sí, juego de palabras). El mapeo key => value puede no haber existido en el array que pasamos a apply, pero ciertamente existe en el arguments variable. Es la misma razón por la que el último ejemplo funciona: Las claves no existen en el objeto que pasamos, pero sí existen en arguments.

¿Por qué es eso? Veamos La sección 15.3.4.3, donde se define Function.prototype.apply. La mayoría de las 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 simple bucle for sobre length elementos, haciendo un list de los valores correspondientes (list es un vudú interno, pero es básicamente un arreglo). 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);
};

Así que todo lo que necesitamos para imitar un argArray en este caso es un objeto con una propiedad length. Y ahora podemos ver por qué los valores son indefinidos, pero las claves no lo son, en arguments: Creamos las asignaciones key=>value.

Uf, así que esto podría no haber sido más corto que la parte anterior. Pero habrá pastel cuando terminemos, así que ten paciencia. Sin embargo, después de la siguiente sección (que será corta, lo prometo) podemos comenzar a diseccionar la expresión. En caso de que lo olvidaras, la pregunta era cómo funciona lo siguiente:

Array.apply(null, { length: 5 }).map(Number.call, Number);

3. Cómo Array maneja múltiples argumentos

So! Vimos lo que sucede cuando se pasa un argumento length 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 dice nosotros qué hacer. El último párrafo es todo lo que nos importa, y está redactado realmente extrañamente, pero se reduce a:{[89]]}

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: {[89]]}

Array.apply(null, { length: 5 })

Vimos que devuelve un array 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, no complicada, ya que no depende tanto de hacks oscuros.

4. Cómo trata Number la entrada

Haciendo Number(something) (la sección 15.7.1 ) convierte something a 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 sección 9.3 en caso de que esté interesado.

5. Juegos de Function.prototype.call

call es el hermano de apply, definido en sección 15.3.4.4. En lugar de tomar una matriz de argumentos, simplemente toma los argumentos que recibió y los pasa hacia adelante.

Las cosas se ponen interesantes cuando encadenas más de uno call juntos, manivela lo raro hasta 11: {[89]]}

function log () {
    console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^  ^-----^
// this   arguments

Esto es bastante digno de wtf hasta que captes lo que está pasando. log.call es solo una función, equivalente al método call de cualquier otra función, y como tal, tiene un método call sobre sí mismo también:

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

¿Y qué hace call? Acepta un thisArg y un montón de argumentos, y llama a su función padre. Podemos definirlo a través de apply (de nuevo, 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);
};

Vamos a rastrear cómo esto sucede: {[89]]}

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 el .map de todo

Todavía no ha terminado. Veamos qué sucede cuando se suministra 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 argumento this nosotros mismos, por defecto es window. Tome nota del orden en el que los argumentos se proporcionan a nuestra devolución de llamada, y vamos a extraño todo el camino a 11 de nuevo:

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. ¿Qué está pasando aquí? Podemos ver en sección 15.4.4.18 , donde se define forEach, lo siguiente sucede prácticamente:

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: {[89]]}

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 funciona .map(Number.call, Number): {[89]]}

Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);

Que devuelve la transformación de i, el índice actual, a un número.

En conclusión,

La expresión

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 repasa esa matriz y toma sus índices, lo que resulta en una matriz de índices de elementos:

[0, 1, 2, 3, 4]
 248
Author: Zirak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-07 12:27:55

Descargo de responsabilidad: Esta es una descripción muy formal del código anterior - así es como yo sé cómo explicarlo. Para una respuesta más simple, consulte la gran respuesta de Zirak anterior. Esta es una especificación más en profundidad en su cara y menos "aha".


Varias cosas están sucediendo aquí. Vamos a separarnos 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 valor this es null que no importa para el constructor de matriz (this es el mismo this que en el contexto de acuerdo con 15.3.4.3.2.a.
  • Entonces new Array se llama pasando un objeto con una propiedad length - que hace que ese objeto sea un array como para todo lo que importa a .apply debido 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, .apply está pasando argumentos de 0 a .length, ya que llamar a [[Get]] en { length: 5 } con los valores 0 a 4 produce undefined el constructor de matriz se llama con cinco argumentos cuyo valor es undefined (obteniendo una propiedad no declarada de un objeto).
  • El constructor de matriz se llama con 0, 2 o más argumentos. La propiedad length de la matriz recién construida se establece en el número de argumentos de acuerdo con la especificación y los valores a los mismos valores.
  • Así 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 comportamiento de .map (8.b) y específicamente [[HasProperty].

Así que 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 pasamos a la segunda parte.

  • Array.prototype.map llama a la devolución de llamada función (en este caso Number.call) en cada elemento de la matriz y utiliza el valor this especificado (en este caso estableciendo el valor this a `Number).
  • El segundo parámetro de la devolución de llamada en map (en este caso Number.call) es el index, y el primero es el valor this.
  • Esto significa que Number se llama con this como undefined (el valor de la matriz) y el índice como el parámetro. Así que es básicamente lo mismo que asignar cada undefined a su índice de matriz (desde que se llama Number realiza conversión de tipo, en este caso de número a número no cambiando 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.

 20
Author: Benjamin Gruenbaum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-03 08:56:02

Como usted dijo, la primera parte:{[16]]}

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

Crea una matriz de 5 valores undefined.

La segunda parte está llamando a la función map 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 toma 3 argumentos y devuelve un valor. Por ejemplo:

function foo(a,b,c){
    ...
    return ...
}

Si pasamos la función foo como la primera 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 map toma se pasa a la función que se pasa como primer argumento. Pero no sería a, b, ni c en el caso de foo, sería 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 hacerlo más claro:

function qux(a,b,c){
    return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]

Entonces qué pasa con el Número.¿llamar ?

Number.call es una función que toma 2 argumentos, y trata de analizar el segundo argumento a un número (no estoy seguro de lo que hace con el primer argumento).

Dado que el segundo argumento que map está pasando es el índice, el valor que se colocará en el nuevo array 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 el mismo valor.

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

 5
Author: Tal Z,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-22 21:13:03

Una matriz es simplemente un objeto que comprende el campo 'length' y algunos métodos (por ejemplo, push). Así que arr en var arr = { length: 5} es básicamente lo mismo que una matriz donde los campos 0..4 tiene el valor predeterminado que es indefinido (es decir, arr[0] === undefined produce true).
En cuanto a la segunda parte, map, como su nombre indica, mapea de una matriz a una nueva. Lo hace recorriendo la matriz original e invocando la función de asignación en cada elemento.

Todo lo que queda es convencerte de que el resultado de mapping-function es el índice. El truco es usar el método llamado' call ' (*) que invoca una función con la pequeña excepción de que el primer parámetro se establece como el contexto 'this', y el segundo se convierte en el primer parámetro (y así sucesivamente). Coincidentemente, cuando se invoca la función mapping, 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 esta (Número) espera la primera param para ser el valor.

(*) se encuentra en el prototipo de la función (y el Número es una función).

MASHAL

 0
Author: shex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-22 22:02:33