web-dev-qa-db-fra.com

Création d'une plage en JavaScript - étrange syntaxe

J'ai rencontré le code suivant dans la liste de diffusion es-discuter:

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

Cela produit

[0, 1, 2, 3, 4]

Pourquoi est-ce le résultat du code? Qu'est-ce qu'il se passe ici?

126
Benjamin Gruenbaum

Comprendre ce "hack" nécessite de comprendre plusieurs choses:

  1. Pourquoi nous ne faisons pas que Array(5).map(...)
  2. Comment Function.prototype.apply Gère les arguments
  3. Comment Array gère plusieurs arguments
  4. Comment la fonction Number gère les arguments
  5. Que fait Function.prototype.call

Ce sont des sujets assez avancés en javascript, donc ce sera plus que plutôt long. Nous allons commencer par le haut. Bouclez!

1. Pourquoi ne pas simplement Array(5).map?

Qu'est-ce qu'un tableau, vraiment? Un objet normal, contenant des clés entières, qui mappent sur des valeurs. Il a d'autres fonctionnalités spéciales, par exemple la variable magique length, mais à la base, c'est une carte key => value Normale, comme tout autre objet. Jouons un peu avec les tableaux, d'accord?

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

Nous arrivons à la différence inhérente entre le nombre d'éléments dans le tableau, arr.length, Et le nombre de mappages key=>value Du tableau, qui peut être différent de arr.length.

L'expansion du tableau via arr.length ne crée pas de nouveaux mappages key=>value, Ce n'est donc pas que le tableau ait des valeurs indéfinies , il ne possède pas ces clés . Et que se passe-t-il lorsque vous essayez d'accéder à une propriété inexistante? Vous obtenez undefined.

Maintenant, nous pouvons lever la tête un peu et voir pourquoi des fonctions comme arr.map Ne parcourent pas ces propriétés. Si arr[3] Était simplement indéfini et que la clé existait, toutes ces fonctions de tableau passeraient simplement dessus comme toute autre valeur:

//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"]

J'ai intentionnellement utilisé un appel de méthode pour prouver davantage que la clé elle-même n'a jamais été là: L'appel de undefined.toUpperCase Aurait déclenché une erreur, mais ce n'est pas le cas. Pour prouver 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

Et maintenant nous arrivons à mon point: comment Array(N) fait les choses. Section 15.4.2.2 décrit le processus. Il y a un tas de mumbo jumbo dont nous ne nous soucions pas, mais si vous parvenez à lire entre les lignes (ou vous pouvez simplement me faire confiance sur celui-ci, mais ne le faites pas), cela se résume essentiellement à ceci:

function Array(len) {
    var ret = [];
    ret.length = len;
    return ret;
}

(fonctionne sous l'hypothèse (qui est vérifiée dans les spécifications réelles) que len est un uint32 valide, et pas n'importe quel nombre de valeurs)

Alors maintenant, vous pouvez voir pourquoi faire Array(5).map(...) ne fonctionnerait pas - nous ne définissons pas les éléments len sur le tableau, nous ne créons pas les mappages key => value, Nous modifiez simplement la propriété length.

Maintenant que nous avons cela à l'écart, regardons la deuxième chose magique:

2. Comment Function.prototype.apply Fonctionne

Ce que apply fait, c'est essentiellement prendre un tableau et le dérouler comme arguments d'appel de fonction. Cela signifie que les éléments suivants sont à peu près les mêmes:

function foo (a, b, c) {
    return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3

Maintenant, nous pouvons faciliter le processus de voir comment apply fonctionne en enregistrant simplement la variable spéciale 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]

Il est facile de prouver ma réclamation dans l'avant-dernier exemple:

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

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

(oui, jeu de mots voulu). Le mappage key => value Peut ne pas avoir existé dans le tableau que nous avons transmis à apply, mais il existe certainement dans la variable arguments. C'est la même raison pour laquelle le dernier exemple fonctionne: Les clés n'existent pas sur l'objet que nous passons, mais elles existent dans arguments.

Pourquoi donc? Regardons Section 15.3.4. , où Function.prototype.apply Est défini. Surtout des choses qui ne nous intéressent pas, mais voici la partie intéressante:

  1. Soit len ​​le résultat de l'appel de la méthode interne [[Get]] de argArray avec l'argument "length".

Ce qui signifie essentiellement: argArray.length. La spécification procède ensuite à une simple boucle for sur length éléments, créant un list de valeurs correspondantes (list est un vaudou interne, mais c'est essentiellement un tableau). En termes de code très, très lâche:

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

Donc, tout ce dont nous avons besoin pour imiter un argArray dans ce cas est un objet avec une propriété length. Et maintenant, nous pouvons voir pourquoi les valeurs ne sont pas définies, mais les clés ne le sont pas, sur arguments: Nous créons les mappages key=>value.

Ouf, donc cela n'a peut-être pas été plus court que la partie précédente. Mais il y aura du gâteau à la fin, alors soyez patient! Cependant, après la section suivante (qui sera courte, je le promets), nous pouvons commencer à disséquer l'expression. Au cas où vous auriez oublié, la question était de savoir comment fonctionne le suivant:

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

3. Comment Array gère plusieurs arguments

Alors! Nous avons vu ce qui se passe lorsque vous passez un argument length à Array, mais dans l'expression, nous passons plusieurs choses comme arguments (un tableau de 5 undefined, pour être exact) . Section 15.4.2.1 nous dit quoi faire. Le dernier paragraphe est tout ce qui compte pour nous, et il est formulé vraiment bizarrement, mais il se résume en quelque sorte à:

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! Nous obtenons un tableau de plusieurs valeurs non définies, et nous retournons un tableau de ces valeurs non définies.

La première partie de l'expression

Enfin, nous pouvons déchiffrer les éléments suivants:

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

Nous avons vu qu'il retourne un tableau contenant 5 valeurs indéfinies, avec des clés toutes existantes.

Maintenant, à la deuxième partie de l'expression:

[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)

Ce sera la partie la plus facile et non alambiquée, car elle ne dépend pas tellement de hacks obscurs.

4. Comment Number traite les entrées

Faire Number(something) ( section 15.7.1 ) convertit something en un nombre, et c'est tout. Comment cela se fait est un peu compliqué, surtout dans le cas des chaînes, mais l'opération est définie dans section 9. au cas où vous seriez intéressé.

5. Jeux de Function.prototype.call

call est le frère de apply, défini dans section 15.3.4.4 . Au lieu de prendre un tableau d'arguments, il prend simplement les arguments qu'il a reçus et les transmet.

Les choses deviennent intéressantes lorsque vous enchaînez plus d'un call ensemble, lancez l'étrange jusqu'à 11:

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

C'est assez intéressant jusqu'à ce que vous compreniez ce qui se passe. log.call Est juste une fonction, équivalente à la méthode call de toute autre fonction, et en tant que telle, possède également une méthode call:

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

Et que fait call? Il accepte un thisArg et un tas d'arguments et appelle sa fonction parent. Nous pouvons le définir via apply (encore une fois, du code très lâche, ne fonctionnera pas):

Function.prototype.call = function (thisArg) {
    var args = arguments.slice(1); //I wish that'd work
    return this.apply(thisArg, args);
};

Voyons comment cela se passe:

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 dernière partie, ou le .map De tout cela

Ce n'est pas encore fini. Voyons ce qui se passe lorsque vous fournissez une fonction à la plupart des méthodes de tableau:

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 nous ne fournissons pas nous-mêmes un argument this, il prend par défaut window. Prenez note de l'ordre dans lequel les arguments sont fournis pour notre rappel, et remontons jusqu'à 11:

arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^    ^

Whoa whoa whoa ... revenons un peu en arrière. Que se passe t-il ici? Nous pouvons voir dans section 15.4.4.18 , où forEach est défini, ce qui se passe à peu près comme suit:

var callback = log.call,
    thisArg = log;

for (var i = 0; i < arr.length; i += 1) {
    callback.call(thisArg, arr[i], i, arr);
}

Donc, nous obtenons ceci:

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

Maintenant, nous pouvons voir comment .map(Number.call, Number) fonctionne:

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

Ce qui renvoie la transformation de i, l'index actuel, en un nombre.

En conclusion,

L'expression

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

Fonctionne en deux parties:

var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2

La première partie crée un tableau de 5 éléments non définis. Le second passe sur ce tableau et prend ses indices, résultant en un tableau d'indices d'éléments:

[0, 1, 2, 3, 4]
255
Zirak

Avertissement : Ceci est une description très formelle du code ci-dessus - voici comment [~ # ~] i [~ # ~] savoir l'expliquer. Pour une réponse plus simple - consultez l'excellente réponse de Zirak ci-dessus. Il s'agit d'une spécification plus approfondie dans votre visage et moins "aha".


Plusieurs choses se produisent ici. Brisons-le un peu.

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

Dans la première ligne, le constructeur du tableau est appelé en tant que fonction avec Function.prototype.apply .

  • La valeur this est null ce qui n'a pas d'importance pour le constructeur Array (this est le même this que dans le contexte selon 15.3.4.3.2 .une.
  • Ensuite, new Array Est appelé en passant un objet avec une propriété length - qui fait que cet objet soit un tableau comme pour tout ce qui compte pour .apply En raison de la clause suivante dans .apply:
    • Soit len ​​le résultat de l'appel de la méthode interne [[Get]] de argArray avec l'argument "length".
  • En tant que tel, .apply Transmet des arguments de 0 à .length, Car appeler [[Get]] Sur { length: 5 } Avec les valeurs 0 à 4 donne undefined le constructeur du tableau est appelé avec cinq arguments dont la valeur est undefined (obtention d'une propriété non déclarée d'un objet).
  • Le constructeur du tableau est appelé avec 0, 2 ou plusieurs arguments . La propriété length du tableau nouvellement construit est définie sur le nombre d'arguments selon la spécification et les valeurs sur les mêmes valeurs.
  • Ainsi var arr = Array.apply(null, { length: 5 }); crée une liste de cinq valeurs indéfinies.

Remarque : notez ici la différence entre Array.apply(0,{length: 5}) et Array(5), la première créant cinq fois le type de valeur primitif undefined et ce dernier créant un tableau vide de longueur 5. Plus précisément, en raison du comportement de .map (8.b) et spécifiquement [[HasProperty] .

Ainsi, le code ci-dessus dans une spécification conforme est le même que:

var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed

Passons maintenant à la deuxième partie.

  • Array.prototype.map appelle la fonction de rappel (dans ce cas Number.call) Sur chaque élément du tableau et utilise la valeur this spécifiée (dans ce cas, la configuration la valeur this à `Number).
  • Le deuxième paramètre du rappel dans la carte (dans ce cas Number.call) Est l'index, et le premier est la valeur this.
  • Cela signifie que Number est appelé avec this comme undefined (la valeur du tableau) et l'index comme paramètre. Donc, c'est fondamentalement la même chose que de mapper chaque undefined à son index de tableau (puisque l'appel Number effectue une conversion de type, dans ce cas d'un nombre à l'autre sans changer l'index) .

Ainsi, le code ci-dessus prend les cinq valeurs non définies et mappe chacune à son index dans le tableau.

C'est pourquoi nous obtenons le résultat de notre code.

20
Benjamin Gruenbaum

Comme vous l'avez dit, la première partie:

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

crée un tableau de 5 undefined valeurs.

La deuxième partie appelle la fonction map du tableau qui prend 2 arguments et retourne un nouveau tableau de la même taille.

Le premier argument que map prend est en fait une fonction à appliquer sur chaque élément du tableau, il devrait être une fonction qui prend 3 arguments et renvoie une valeur. Par exemple:

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

si nous passons la fonction foo comme premier argument, elle sera appelée pour chaque élément avec

  • a comme valeur de l'élément itéré actuel
  • b comme index de l'élément itéré actuel
  • c comme tout le tableau d'origine

Le deuxième argument pris par map est passé à la fonction que vous passez comme premier argument. Mais ce ne serait pas a, b, ni c dans le cas de foo, ce serait this.

Deux exemples:

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]

et un autre juste pour le rendre plus clair:

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

Qu'en est-il de Number.call?

Number.call est une fonction qui prend 2 arguments et essaie d'analyser le deuxième argument en un nombre (je ne suis pas sûr de ce qu'il fait avec le premier argument).

Puisque le deuxième argument que map passe est l'index, la valeur qui sera placée dans le nouveau tableau à cet index est égale à l'index. Tout comme la fonction baz dans l'exemple ci-dessus. Number.call essaiera d'analyser l'index - il retournera naturellement la même valeur.

Le deuxième argument que vous avez passé à la fonction map dans votre code n'a en fait aucun effet sur le résultat. Corrigez-moi si je me trompe, s'il vous plaît.

5
Tal Z

Un tableau est simplement un objet comprenant le champ `` longueur '' et certaines méthodes (par exemple Push). Alors arr dans var arr = { length: 5} est fondamentalement la même chose qu'un tableau où les champs 0..4 ont la valeur par défaut qui n'est pas définie (c'est-à-dire arr[0] === undefined donne vrai).
Comme pour la deuxième partie, mappez, comme son nom l'indique, mappe d'un tableau à un nouveau. Il le fait en parcourant le tableau d'origine et en invoquant la fonction de mappage sur chaque élément.

Il ne vous reste plus qu'à vous convaincre que le résultat de la fonction de cartographie est l'index. L'astuce consiste à utiliser la méthode nommée 'call' (*) qui invoque une fonction avec la petite exception que le premier paramètre est défini pour être le contexte 'this', et le second devient le premier param (et ainsi de suite). Par coïncidence, lorsque la fonction de mappage est invoquée, le deuxième paramètre est l'index.

Enfin et surtout, la méthode qui est invoquée est la Number "Class", et comme nous le savons dans JS, une "Class" est simplement une fonction, et celle-ci (Number) s'attend à ce que le premier paramètre soit la valeur.

(*) trouvé dans le prototype de Function (et Number est une fonction).

MASHAL

0
shex