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?
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?
Réponses:
Comprendre ce "hack" nécessite de comprendre plusieurs choses:
Array(5).map(...)Function.prototype.applygère les argumentsArraygère plusieurs argumentsNumberfonction gère les argumentsFunction.prototype.callfaitCe 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!
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 lengthvariable magique , mais au fond, c'est une key => valuecarte régulière , 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=>valuemappages du tableau, qui peut être différent de arr.length.
Le développement du tableau via arr.length ne crée pas de nouveaux key=>valuemappages, 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 obtenez undefined.
Maintenant, nous pouvons lever un peu la tête et voir pourquoi des fonctions comme arr.mapne pas marcher sur ces propriétés. Si elle arr[3]était simplement indéfinie et que la clé existait, toutes ces fonctions de tableau la parcourraient 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 le fait que la clé elle-même n'était jamais là: l'appel undefined.toUpperCaseaurait 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)ç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:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(fonctionne sous l'hypothèse (qui est vérifiée dans la spécification réelle) qui lenest 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 les lenéléments sur le tableau, nous ne créons pas les key => valuemappages, nous modifions simplement la lengthpropriété.
Maintenant que nous avons cela de côté, regardons la deuxième chose magique:
Function.prototype.applyfonctionneCe qui applyfait, 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:
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 applyfonctionne en enregistrant simplement la argumentsvariable 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 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 prévu). Le key => valuemappage n'a peut-être pas existé dans le tableau auquel nous sommes passés apply, mais il existe certainement dans la argumentsvariable. 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 la section 15.3.4.3 , où Function.prototype.applyest définie. Surtout des 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 signifie essentiellement: argArray.length. La spécification procède ensuite à une simple forboucle sur les lengthéléments, en faisant un listdes valeurs correspondantes ( listest un vaudou 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 argArraydans ce cas est un objet avec une lengthpropriété. Et maintenant, nous pouvons voir pourquoi les valeurs ne sont pas définies, mais les clés ne le sont pas arguments: nous créons les key=>valuemappages.
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:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Arraygère plusieurs argumentsAlors! Nous avons vu ce qui se passe lorsque vous passez un lengthargument à Array, mais dans l'expression, nous passons plusieurs choses en arguments (un tableau de 5 undefined, 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 à:
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 renvoyons un tableau de ces valeurs indéfinies.
Enfin, nous pouvons déchiffrer ce qui suit:
Array.apply(null, { length: 5 })
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:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Ce sera la partie la plus facile et non alambiquée, car elle ne repose pas tellement sur des hacks obscurs.
Numbertraite l'entréeFaire Number(something)( section 15.7.1 ) convertit somethingen 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.
Function.prototype.callcallest applyle 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 callensemble, augmentez l'étrange à 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Cela vaut vraiment la peine tant que vous ne comprenez pas ce qui se passe. log.callest juste une fonction, équivalente à la callméthode de toute autre fonction , et en tant que telle, a également une callméthode sur elle-même:
log.call === log.call.call; //true
log.call === Function.call; //true
Et que fait call-on? Il accepte un thisArget un tas d'arguments et appelle sa fonction parente. Nous pouvons le définir via apply (encore une fois, un 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}])
.mapde toutCe 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 d' thisargument nous-mêmes, la valeur par défaut est window. Prenez note de l'ordre dans lequel les arguments sont fournis à notre rappel, et répétons-le 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 ... reculons un peu. Que se passe t-il ici? Nous pouvons voir dans la section 15.4.4.18 , où forEachest défini, ce qui suit à peu près se produit:
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.
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 parcourt ce tableau et prend ses indices, ce qui donne un tableau d'indices d'élément:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true. Pourquoi revient-il 2et truerespectivement? Ne passez-vous pas un seul argument, c'est-à-dire Array(2)ici?
apply, mais cet argument est "splatté" en deux arguments passés à la fonction. Vous pouvez le voir plus facilement dans les premiers applyexemples. Le premier console.logmontre alors qu'en effet, nous avons reçu deux arguments (les deux éléments du tableau), et le second console.logmontre que le tableau a un key=>valuemappage 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.
thisdéfaut uniquement Windowen 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.
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é en tant que fonction avec Function.prototype.apply.
thisvaleur est nullqui n'a pas d'importance pour le constructeur Array ( thisest la même thisque dans le contexte selon 15.3.4.3.2.a.new Arrayon appelle passer un objet avec une lengthpropriété - qui fait de cet objet un tableau comme pour tout ce qui compte en .applyraison de la clause suivante dans .apply:
.applyest le passage d' arguments de 0 à .length, étant donné que l' appel [[Get]]sur { length: 5 }les valeurs 0 à 4 rendements undefineddu constructeur de tableau est appelé avec cinq arguments dont la valeur est undefined(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})et Array(5), le premier créant cinq fois le type de valeur primitive undefinedet le second créant un tableau vide de longueur 5. Plus précisément, à cause du .mapcomportement de (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.mapappelle la fonction de rappel (dans ce cas Number.call) sur chaque élément du tableau et utilise la thisvaleur spécifiée (dans ce cas, définissant la thisvaleur sur `Number).Number.call) est l'index, et le premier est la valeur this.Numberest appelé avec thisas undefined(la valeur du tableau) et l'index comme paramètre. C'est donc fondamentalement la même chose que de mapper chacun undefinedà son index de tableau (puisque l'appel Numbereffectue 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.
Array.apply(null,[2])est comme Array(2)qui crée un tableau vide de longueur 2 et non un tableau contenant la valeur primitive undefineddeux 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 le Arrayconstructeur 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 rendement undefinedest alors inséré. Nice trick :)
Comme vous l'avez dit, la première partie:
var arr = Array.apply(null, { length: 5 });
crée un tableau de 5 undefinedvaleurs.
La seconde partie appelle la mapfonction du tableau qui prend 2 arguments et renvoie un nouveau tableau de même taille.
Le premier argument qui mapprend 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:
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 mapprend est passé à la fonction que vous passez comme premier argument. Mais ce ne serait pas a, b, ni c en cas de foo, ça le 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]
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 mappasse est l'index, la valeur qui sera placée dans le nouveau tableau à cet index est égale à l'index. Tout comme la fonction bazde l'exemple ci-dessus. Number.callessaiera d'analyser l'index - il renverra naturellement la même valeur.
Le deuxième argument que vous avez passé à la mapfonction 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.
Number.calln'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é comme this-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 qui arr[0] === undefineddonne 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
[undefined, undefined, undefined, …]et new Array(n)ou {length: n}- ces derniers sont rares , c'est-à-dire qu'ils n'ont aucun élément. C'est très pertinent pour map, et c'est pourquoi l'impair a Array.applyété utilisé.
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.