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 qu'il se passe ici?
javascript
ecmascript-5
Benjamin Gruenbaum
la source
la source
Array.apply(null, Array(30)).map(Number.call, Number)
est plus facile à lire car il évite de prétendre qu'un objet simple est un tableau.Réponses:
Comprendre ce "hack" nécessite de comprendre plusieurs choses:
Array(5).map(...)
Function.prototype.apply
gère les argumentsArray
gère plusieurs argumentsNumber
fonction gère les argumentsFunction.prototype.call
faitCe sont des sujets assez avancés en javascript, donc ce sera plus que plutôt long. Nous allons commencer par le haut. Bouclez votre ceinture!
1. Pourquoi pas simplement
Array(5).map
?Qu'est-ce qu'un tableau, vraiment? Un objet normal, contenant des clés entières, qui correspondent à des valeurs. Il a d'autres fonctionnalités spéciales, par exemple la
length
variable magique , mais au fond, c'est unekey => value
carte régulière , comme n'importe quel autre objet. Jouons un peu avec les tableaux, d'accord?Nous arrivons à la différence inhérente entre le nombre d'éléments dans le tableau
arr.length
, et le nombre dekey=>value
mappages du tableau, qui peut être différent dearr.length
.Le développement du tableau via
arr.length
ne crée pas de nouveauxkey=>value
mappages, donc ce n'est pas que le tableau a des valeurs non définies, il n'a pas ces clés . Et que se passe-t-il lorsque vous essayez d'accéder à une propriété inexistante? Vous obtenezundefined
.Maintenant, nous pouvons lever un peu la tête et voir pourquoi des fonctions comme
arr.map
ne pas marcher sur ces propriétés. Si ellearr[3]
était simplement indéfinie et que la clé existait, toutes ces fonctions de tableau la parcourraient comme toute autre valeur:J'ai intentionnellement utilisé un appel de méthode pour prouver davantage le fait que la clé elle-même n'était jamais là: l'appel
undefined.toUpperCase
aurait soulevé une erreur, mais ce n'est pas le cas. Pour prouver que :Et maintenant nous arrivons à mon point: comment
Array(N)
ça va. La section 15.4.2.2 décrit le processus. Il y a un tas de jumbo mumbo 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:(fonctionne sous l'hypothèse (qui est vérifiée dans la spécification réelle) qui
len
est un uint32 valide, et pas n'importe quel nombre de valeur)Alors maintenant, vous pouvez voir pourquoi cela
Array(5).map(...)
ne fonctionnerait pas - nous ne définissons pas leslen
éléments sur le tableau, nous ne créons pas leskey => value
mappages, nous modifions simplement lalength
propriété.Maintenant que nous avons cela de côté, regardons la deuxième chose magique:
2. Comment
Function.prototype.apply
fonctionneCe qui
apply
fait, c'est prendre un tableau et le dérouler en tant qu'arguments d'un appel de fonction. Cela signifie que les éléments suivants sont à peu près les mêmes:Maintenant, nous pouvons faciliter le processus de voir comment
apply
fonctionne en enregistrant simplement laarguments
variable spéciale:Il est facile de prouver ma réclamation dans l'avant-dernier exemple:
(oui, jeu de mots prévu). Le
key => value
mappage n'a peut-être pas existé dans le tableau auquel nous sommes passésapply
, mais il existe certainement dans laarguments
variable. 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 dansarguments
.Pourquoi donc? Regardons la section 15.3.4.3 , où
Function.prototype.apply
est définie. Surtout des choses qui ne nous intéressent pas, mais voici la partie intéressante:Ce qui signifie essentiellement:
argArray.length
. La spécification procède ensuite à une simplefor
boucle sur leslength
éléments, en faisant unlist
des valeurs correspondantes (list
est un vaudou interne, mais c'est fondamentalement un tableau). En termes de code très, très lâche:Donc, tout ce dont nous avons besoin pour imiter un
argArray
dans ce cas est un objet avec unelength
propriété. Et maintenant, nous pouvons voir pourquoi les valeurs ne sont pas définies, mais les clés ne le sont pasarguments
: nous créons leskey=>value
mappages.Ouf, cela n'aurait peut-être pas été plus court que la partie précédente. Mais il y aura du gâteau quand nous aurons fini, 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 ce qui suit:
3. Comment
Array
gère plusieurs argumentsAlors! Nous avons vu ce qui se passe lorsque vous passez un
length
argument àArray
, mais dans l'expression, nous passons plusieurs choses en arguments (un tableau de 5undefined
, pour être exact). La section 15.4.2.1 nous dit quoi faire. Le dernier paragraphe est tout ce qui compte pour nous, et il est formulé de manière très étrange, mais il se résume en quelque sorte à:Tada! Nous obtenons un tableau de plusieurs valeurs indéfinies, et nous renvoyons un tableau de ces valeurs indéfinies.
La première partie de l'expression
Enfin, nous pouvons déchiffrer ce qui suit:
Nous avons vu qu'il renvoie un tableau contenant 5 valeurs indéfinies, avec des clés toutes existantes.
Passons maintenant à la deuxième partie de l'expression:
Ce sera la partie la plus facile et non alambiquée, car elle ne repose pas tellement sur des hacks obscurs.
4. Comment
Number
traite l'entréeFaire
Number(something)
( section 15.7.1 ) convertitsomething
en nombre, et c'est tout. Comment faire cela est un peu compliqué, en particulier dans le cas des chaînes, mais l'opération est définie dans la section 9.3 au cas où cela vous intéresserait.5. Jeux de
Function.prototype.call
call
estapply
le frère de, défini à la section 15.3.4.4 . Au lieu de prendre un tableau d'arguments, il prend simplement les arguments reçus et les transmet.Les choses deviennent intéressantes lorsque vous en chaînez plus d'un
call
ensemble, augmentez l'étrange à 11:Cela vaut vraiment la peine tant que vous ne comprenez pas ce qui se passe.
log.call
est juste une fonction, équivalente à lacall
méthode de toute autre fonction , et en tant que telle, a également unecall
méthode sur elle-même:Et que fait
call
-on? Il accepte unthisArg
et un tas d'arguments et appelle sa fonction parente. Nous pouvons le définir viaapply
(encore une fois, un code très lâche, ne fonctionnera pas):Voyons comment cela se passe:
La dernière partie, ou le
.map
de toutCe n'est pas encore fini. Voyons ce qui se passe lorsque vous fournissez une fonction à la plupart des méthodes de tableau:
Si nous ne fournissons pas d'
this
argument nous-mêmes, la valeur par défaut estwindow
. Prenez note de l'ordre dans lequel les arguments sont fournis à notre rappel, et répétons-le jusqu'à 11:Whoa whoa whoa ... reculons un peu. Que se passe t-il ici? Nous pouvons voir dans la section 15.4.4.18 , où
forEach
est défini, ce qui suit à peu près se produit:Donc, nous obtenons ceci:
Maintenant, nous pouvons voir comment
.map(Number.call, Number)
fonctionne:Ce qui renvoie la transformation de
i
, l'index actuel, en un nombre.En conclusion,
L'expression
Fonctionne en deux parties:
La première partie crée un tableau de 5 éléments non définis. Le second parcourt ce tableau et prend ses indices, ce qui donne un tableau d'indices d'élément:
la source
ahaExclamationMark.apply(null, Array(2)); //2, true
. Pourquoi revient-il2
ettrue
respectivement? Ne passez-vous pas un seul argument, c'est-à-direArray(2)
ici?apply
, mais cet argument est "splatté" en deux arguments passés à la fonction. Vous pouvez le voir plus facilement dans les premiersapply
exemples. Le premierconsole.log
montre alors qu'en effet, nous avons reçu deux arguments (les deux éléments du tableau), et le secondconsole.log
montre que le tableau a unkey=>value
mappage dans le 1er slot (comme expliqué dans la 1ère partie de la réponse).log.apply(null, document.getElementsByTagName('script'));
n'est pas nécessaire pour fonctionner et ne fonctionne pas dans certains navigateurs, et[].slice.call(NodeList)
transformer une NodeList en un tableau ne fonctionnera pas non plus.this
défaut uniquementWindow
en mode non strict.Avertissement : Ceci est une description très formelle du code ci-dessus - c'est ainsi que je sais comment l'expliquer. Pour une réponse plus simple, consultez la bonne réponse de Zirak ci-dessus. Ceci est une spécification plus approfondie dans votre visage et moins "aha".
Plusieurs choses se passent ici. Disons-le un peu.
Dans la première ligne, le constructeur de tableau est appelé en tant que fonction avec
Function.prototype.apply
.this
valeur estnull
qui n'a pas d'importance pour le constructeur Array (this
est la mêmethis
que dans le contexte selon 15.3.4.3.2.a.new Array
on appelle passer un objet avec unelength
propriété - qui fait de cet objet un tableau comme pour tout ce qui compte en.apply
raison de la clause suivante dans.apply
:.apply
est le passage d' arguments de 0 à.length
, étant donné que l' appel[[Get]]
sur{ length: 5 }
les valeurs 0 à 4 rendementsundefined
du constructeur de tableau est appelé avec cinq arguments dont la valeur estundefined
(obtenir une propriété d'un objet non déclaré).var arr = Array.apply(null, { length: 5 });
Crée ainsi une liste de cinq valeurs non définies.Remarque : Notez ici la différence entre
Array.apply(0,{length: 5})
etArray(5)
, le premier créant cinq fois le type de valeur primitiveundefined
et le second créant un tableau vide de longueur 5. Plus précisément, à cause du.map
comportement de (8.b) et spécifiquement[[HasProperty]
.Ainsi, le code ci-dessus dans une spécification conforme est le même que:
Passons maintenant à la deuxième partie.
Array.prototype.map
appelle la fonction de rappel (dans ce casNumber.call
) sur chaque élément du tableau et utilise lathis
valeur spécifiée (dans ce cas, définissant lathis
valeur sur `Number).Number.call
) est l'index, et le premier est la valeur this.Number
est appelé avecthis
asundefined
(la valeur du tableau) et l'index comme paramètre. C'est donc fondamentalement la même chose que de mapper chacunundefined
à son index de tableau (puisque l'appelNumber
effectue une conversion de type, dans ce cas, du nombre au nombre ne change pas 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 dans notre code.
la source
Array.apply(null, { length: 2 })
et non avecArray.apply(null, [2])
qui appellerait également leArray
constructeur passant2
comme valeur de longueur? fiddleArray.apply(null,[2])
est commeArray(2)
qui crée un tableau vide de longueur 2 et non un tableau contenant la valeur primitiveundefined
deux fois. Voir ma dernière modification dans la note après la première partie, faites-moi savoir si elle est suffisamment claire et sinon je clarifierai cela.{length: 2}
simule un tableau avec deux éléments que leArray
constructeur insérerait dans le tableau nouvellement créé. Comme il n'y a pas de tableau réel accédant aux éléments non présents, le rendementundefined
est alors inséré. Nice trick :)Comme vous l'avez dit, la première partie:
crée un tableau de 5
undefined
valeurs.La seconde partie appelle la
map
fonction du tableau qui prend 2 arguments et renvoie un nouveau tableau de même taille.Le premier argument qui
map
prend est en fait une fonction à appliquer sur chaque élément du tableau, on s'attend à ce que ce soit une fonction qui prend 3 arguments et renvoie une valeur. Par exemple:si nous passons la fonction foo comme premier argument, elle sera appelée pour chaque élément avec
Le deuxième argument qui
map
prend est passé à la fonction que vous passez comme premier argument. Mais ce ne serait pas a, b, ni c en cas defoo
, ça le seraitthis
.Deux exemples:
et un autre juste pour que ce soit plus clair:
Alors 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).Étant donné que le deuxième argument qui
map
passe est l'index, la valeur qui sera placée dans le nouveau tableau à cet index est égale à l'index. Tout comme la fonctionbaz
de l'exemple ci-dessus.Number.call
essaiera d'analyser l'index - il renverra naturellement la même valeur.Le deuxième argument que vous avez passé à la
map
fonction dans votre code n'a pas réellement d'effet sur le résultat. Corrigez-moi si je me trompe, s'il vous plaît.la source
Number.call
n'est pas une fonction spéciale qui analyse les arguments en nombres. C'est juste=== Function.prototype.call
. Seul le second argument, la fonction qui est passé commethis
-value àcall
, est pertinente -.map(eval.call, Number)
,.map(String.call, Number)
et.map(Function.prototype.call, Number)
sont tous équivalents.Un tableau est simplement un objet comprenant le champ 'length' et certaines méthodes (par exemple push). Donc arr in
var arr = { length: 5}
est fondamentalement le même qu'un tableau où les champs 0..4 ont la valeur par défaut qui n'est pas définie (c'est-à-dire quiarr[0] === undefined
donne vrai).Quant à 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 reste plus qu'à vous convaincre que le résultat de la fonction de mappage est l'index. L'astuce consiste à utiliser la méthode nommée 'call' (*) qui invoque une fonction à la petite exception que le premier paramètre est défini comme étant le contexte 'this' et le second devient le premier paramètre (et ainsi de suite). Par coïncidence, lorsque la fonction de mappage est appelée, le deuxième paramètre est l'index.
Last but not least, la méthode qui est invoquée est le 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
la source
[undefined, undefined, undefined, …]
etnew Array(n)
ou{length: n}
- ces derniers sont rares , c'est-à-dire qu'ils n'ont aucun élément. C'est très pertinent pourmap
, et c'est pourquoi l'impair aArray.apply
été utilisé.