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 ?
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 ?
Pour comprendre ce "hack", il faut comprendre plusieurs choses :
Array(5).map(...)
Function.prototype.apply
gère les argumentsArray
gère les arguments multiplesNumber
La fonction gère les argumentsFunction.prototype.call
faitCe sont des sujets assez avancés en javascript, donc ce sera plus-que-peu long. Nous allons commencer par le début. Accrochez-vous !
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 :
Function.prototype.apply
travauxQuoi 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 :
- 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);
Array
gère les arguments multiplesDonc ! 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.
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.
Number
traite l'entréeFaire 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é.
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}])
.map
de tout celaCe 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.
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]
@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 ?
@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).
En raison de (certaines) demandes vous pouvez maintenant profiter de la version audio : dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
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
.
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.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
:
.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).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").Number.call
) est l'indice, et le premier est cette valeur.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.
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/
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
@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.
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
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.
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.
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
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.
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.
1 votes
Je ne vois pas pourquoi on voudrait faire ça. Le temps qu'il faut pour créer le tableau de cette façon aurait pu être fait d'une manière un peu moins sexy mais beaucoup plus rapide : jsperf.com/basic-vs-extrême