131 votes

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

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

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 qui se passe ici ?

2 votes

OMI Array.apply(null, Array(30)).map(Number.call, Number) est plus facile à lire car elle évite de prétendre qu'un objet ordinaire est un tableau.

10 votes

@fncomp S'il vous plaît n'utilisez pas l'un ou l'autre pour en fait créer une plage. Non seulement cette méthode est plus lente que l'approche directe, mais elle est également loin d'être aussi facile à comprendre. Il est difficile de comprendre la syntaxe (enfin, vraiment l'API et non la syntaxe) ici, ce qui en fait une question intéressante mais un code de production terrible IMO.

0 votes

Oui, je ne suggère pas que quelqu'un l'utilise, mais je pensais que c'était quand même plus facile à lire, par rapport à la version littérale de l'objet.

271voto

Zirak Points 13656

Pour comprendre ce "hack", il faut 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 les arguments multiples
  4. Comment le Number La fonction gère les arguments
  5. Quoi Function.prototype.call fait

Ce sont des sujets assez avancés en javascript, donc ce sera plus-que-peu long. Nous allons commencer par le début. Accrochez-vous !

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

Qu'est-ce qu'un tableau, en fait ? Un objet ordinaire, contenant des clés entières, qui correspondent à des valeurs. Il possède d'autres caractéristiques spéciales, par exemple la fonction magique length variable, mais au fond, il s'agit d'une variable régulière. key => value map, comme n'importe quel 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 key=>value les mappings du tableau, qui peuvent être différents de ceux de l'UE. arr.length .

L'extension du tableau par arr.length n'est pas créer tout nouveau key=>value mappings, donc ce n'est pas que le tableau a des valeurs indéfinies, c'est que 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 un peu la tête, et voir pourquoi des fonctions telles que arr.map ne marchez pas sur ces propriétés. Si arr[3] était simplement indéfinie, et que la clé existait, toutes ces fonctions de tableau l'auraient simplement parcourue comme n'importe quelle 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 encore plus que la clé elle-même n'a jamais été là : Appel à undefined.toUpperCase aurait soulevé 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 des choses. Section 15.4.2.2 décrit le processus. Il y a un tas de charabia dont nous ne nous soucions pas, mais si vous arrivez à lire entre les lignes (ou si vous pouvez me faire confiance sur ce point, mais ne le faites pas), cela se résume essentiellement à ceci :

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

(fonctionne en partant du principe (qui est vérifié dans la spécification actuelle) que len est un uint32 valide, et pas n'importe quel nombre de valeur)

Donc maintenant vous pouvez voir pourquoi faire Array(5).map(...) ne fonctionnerait pas - nous ne définissons pas len dans le tableau, nous ne créons pas l'élément key => value nous modifions simplement les length propriété.

Maintenant que nous avons réglé cette question, examinons la deuxième chose magique :

2. Comment Function.prototype.apply travaux

Quoi apply est de prendre un tableau et de le dérouler comme arguments d'un 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 pour voir comment apply fonctionne en enregistrant simplement le arguments variable spéciale :

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 mon affirmation 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). Le site key => value peut ne pas avoir existé dans le tableau que nous avons transmis à la Commission européenne. apply mais elle existe certainement dans le arguments variable. C'est pour la même raison que le dernier exemple fonctionne : Les clés n'existent pas sur l'objet que nous passons, mais elles existent dans la variable arguments .

Pourquoi ? Regardons Section 15.3.4.3 , donde Function.prototype.apply est défini. La plupart du temps, il s'agit de 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 veut dire en gros : argArray.length . La spécification procède ensuite à une simple for boucle sur length les articles, en faisant un list des valeurs correspondantes ( list est un peu de voodoo interne, mais c'est fondamentalement 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 un length propriété. Et maintenant nous pouvons voir pourquoi les valeurs sont indéfinies, mais les clés ne le sont pas. arguments : Nous créons le key=>value mappings.

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

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

3. Comment Array gère les arguments multiples

Donc ! Nous avons vu ce qui se passe lorsque vous passez une length argument pour 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 ce qu'il faut faire. Le dernier paragraphe est tout ce qui compte pour nous, et il est formulé comme suit . realmente bizarrement, mais ça 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 indéfinies, et nous retournons un tableau de ces valeurs indéfinies.

La première partie de l'expression

Finalement, nous pouvons déchiffrer ce qui suit :

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

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

Passons maintenant à la deuxième partie de l'expression :

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

Il s'agit de la partie la plus facile et la moins compliquée, car elle ne repose pas sur d'obscures bidouilles.

4. Comment Number traite l'entrée

Faire Number(something) ( section 15.7.1 ) convertit something en un nombre, et c'est tout. La manière de procéder est un peu compliquée, surtout dans le cas des chaînes de caractères, mais l'opération est définie dans le fichier section 9.3 au cas où vous seriez intéressé.

5. Jeux de Function.prototype.call

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

Les choses deviennent intéressantes lorsque vous enchaînez plus d'un call ensemble, mets le bizarre jusqu'à 11 :

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

C'est tout à fait digne de wtf jusqu'à ce que vous compreniez ce qui se passe. log.call est juste une fonction, équivalente à n'importe quelle autre fonction. call et, en tant que telle, a une call sur lui-même également :

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

Et qu'est-ce que call faire ? Il accepte un thisArg et un tas d'arguments, et appelle sa fonction parent. Nous pouvons la définir via apply (à nouveau, un code très lâche, qui 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 un this nous-mêmes, la valeur par défaut est window . Prenez note de l'ordre dans lequel les arguments sont fournis à notre callback, et faisons encore une fois des écarts 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. Qu'est-ce qui se passe ici ? Nous pouvons voir dans section 15.4.4.18 , donde forEach est défini, ce qui suit se produit à peu près :

var callback = log.call,
    thisArg = log;

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

Donc, on a ça :

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) travaux :

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

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

En conclusion,

L'expression

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

Œuvre 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 indéfinis. La deuxième partie parcourt ce tableau et prend ses indices, ce qui donne un tableau d'indices d'éléments :

[0, 1, 2, 3, 4]

0 votes

@Zirak Veuillez m'aider à comprendre ce qui suit ahaExclamationMark.apply(null, Array(2)); //2, true . Pourquoi retourne-t-il 2 y true respectivement ? N'êtes-vous pas en train de passer un seul argument c'est-à-dire Array(2) ici ?

4 votes

@Geek Nous ne passons qu'un seul argument à apply mais cet argument est "éclaté" en deux arguments transmis à la fonction. Vous pouvez le voir plus facilement dans la première apply exemples. Le premier console.log montre alors qu'effectivement, nous avons reçu deux arguments (les deux éléments du tableau), et le second console.log montre que le tableau a un key=>value dans le 1er emplacement (comme expliqué dans la 1ère partie de la réponse).

4 votes

En raison de (certaines) demandes vous pouvez maintenant profiter de la version audio : dl.dropboxusercontent.com/u/24522528/SO-answer.mp3

21voto

Benjamin Gruenbaum Points 51406

Avis de non-responsabilité : Il s'agit d'une description très formelle du code ci-dessus - c'est comment I savoir comment 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 et moins "aha".


Plusieurs choses se passent ici. Séparons-les 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 de tableau est appelé comme une fonction con Function.prototype.apply .

  • El this valeur est null ce qui n'a pas d'importance pour le constructeur de tableau ( this est le même this comme dans le contexte selon 15.3.4.3.2.a.
  • Puis new Array est appelé en recevant un objet avec un length qui fait en sorte 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 passe des arguments de 0 à .length puisque l'appel [[Get]] en { length: 5 } avec les valeurs 0 à 4 donne undefined le constructeur de tableau est appelé avec cinq arguments dont la valeur est undefined (obtenir une propriété non déclarée d'un objet).
  • Le constructeur de tableau est appelé avec 0, 2 ou plusieurs arguments . La propriété length du tableau nouvellement construit est fixée au nombre d'arguments selon la spécification et les valeurs aux mêmes valeurs.
  • Ainsi, var arr = Array.apply(null, { length: 5 }); crée une liste de cinq valeurs indéfinies.

Note : Remarquez la différence ici entre Array.apply(0,{length: 5}) y Array(5) le premier créant cinq fois le type de valeur primitive. undefined et le dernier crée un tableau vide de longueur 5. Spécifiquement, à cause de .map (8.b) et plus particulièrement [[HasProperty] .

Donc 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 fonction this (dans ce cas, la valeur this à "Nombre").
  • Le deuxième paramètre de la fonction de rappel dans la carte (dans ce cas, il s'agit de Number.call ) est l'indice, et le premier est cette valeur.
  • Cela signifie que Number est appelé avec this como undefined (la valeur du tableau) et l'index comme paramètre. C'est donc fondamentalement la même chose que de mettre en correspondance chaque fichier undefined à son index de tableau (puisque l'appel de Number effectue une conversion de type, dans ce cas d'un nombre à un nombre sans changer l'index).

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

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

1 votes

Pour les docs : Spécification du fonctionnement de la carte : es5.github.io/#x15.4.4.19 Mozilla propose un exemple de script qui fonctionne conformément à cette spécification à l'adresse suivante developer.mozilla.org/fr/US/docs/Web/JavaScript/Référence/

1 votes

Mais pourquoi cela ne fonctionne-t-il qu'avec Array.apply(null, { length: 2 }) et non Array.apply(null, [2]) qui appellerait également le Array Constructeur passant 2 comme valeur de la longueur ? violon

0 votes

@Andreas Array.apply(null,[2]) c'est comme Array(2) qui crée un vide de longueur 2 et no un tableau contenant la valeur primitive undefined deux fois. Voyez mon édition la plus récente dans la note après la première partie, dites-moi si c'est assez clair et si non je vais clarifier cela.

6voto

talzaj Points 106

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 consiste à appeler le map du tableau qui prend 2 arguments et renvoie un nouveau tableau de même taille.

Le premier argument qui map takes est en fait une fonction à appliquer sur chaque élément du tableau, elle est censée ê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 la valeur de l'élément itéré actuel
  • b comme l'indice de l'élément itéré actuel
  • c comme l'ensemble du tableau original

Le deuxième argument qui map prend est transmis à la fonction que vous passez comme premier argument. Mais ce ne serait pas a, b, ni c dans le cas de foo il 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 que ce soit 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 de transformer le second argument en un nombre (je ne suis pas sûr de ce qu'elle fait avec le premier argument).

Puisque le deuxième argument que map est passée est l'indice, la valeur qui sera placée dans le nouveau tableau à cet indice est égale à l'indice. 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 map dans votre code n'a pas vraiment d'effet sur le résultat. Corrigez-moi si je me trompe, s'il vous plaît.

1 votes

Number.call n'est pas une fonction spéciale qui analyse les arguments en nombres. Il s'agit simplement de === Function.prototype.call . Seul le deuxième argument, la fonction qui est transmise comme le this -valeur à call est pertinente - .map(eval.call, Number) , .map(String.call, Number) y .map(Function.prototype.call, Number) sont toutes équivalentes.

0voto

shex Points 153

Un tableau est simplement un objet comprenant le champ 'length' et quelques méthodes (par exemple push). Ainsi, 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 est indéfinie (c'est-à-dire que arr[0] === undefined c'est vrai).
Quant à la seconde partie, map, comme son nom l'indique, fait correspondre un tableau à un nouveau tableau. Pour ce faire, elle parcourt le tableau d'origine et invoque la fonction de mappage sur chaque élément.

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

Enfin, la méthode invoquée est la "classe" Nombre, et comme nous le savons en JS, une "classe" est simplement une fonction, et celle-ci (Nombre) attend du premier paramètre qu'il soit la valeur.

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

MASHAL

1 votes

Il y a une énorme différence entre [undefined, undefined, undefined, …] y new Array(n) o {length: n} - ces derniers sont éparses c'est-à-dire qu'ils n'ont pas d'éléments. Ceci est très important pour map et c'est pourquoi l'impair Array.apply a été utilisé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X