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.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!
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 une key => value
carte 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=>value
mappages 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=>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 obtenez undefined
.
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 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.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)
ç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 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 les len
éléments sur le tableau, nous ne créons pas les key => value
mappages, nous modifions simplement la length
propriété.
Maintenant que nous avons cela de côté, regardons la deuxième chose magique:
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:
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 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 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 => value
mappage n'a peut-être pas existé dans le tableau auquel nous sommes passés apply
, mais il existe certainement dans la arguments
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 dans arguments
.
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:
- 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 for
boucle sur les length
éléments, en faisant un list
des valeurs correspondantes ( list
est 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 argArray
dans ce cas est un objet avec une length
proprié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=>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:
Array.apply(null, { length: 5 }).map(Number.call, Number);
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 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.
Number
traite l'entréeFaire Number(something)
( section 15.7.1 ) convertit something
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.
Function.prototype.call
call
est apply
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:
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.call
est juste une fonction, équivalente à la call
méthode de toute autre fonction , et en tant que telle, a également une call
méthode sur elle-même:
log.call === log.call.call; //true
log.call === Function.call; //true
Et que fait call
-on? Il accepte un thisArg
et 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}])
.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:
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' this
argument 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ù forEach
est 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 2
et true
respectivement? 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 apply
exemples. Le premier console.log
montre alors qu'en effet, 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
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 uniquement Window
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.
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
.
this
valeur est null
qui n'a pas d'importance pour le constructeur Array ( this
est la même this
que dans le contexte selon 15.3.4.3.2.a.new Array
on appelle passer un objet avec une length
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 rendements undefined
du 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 undefined
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:
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 this
valeur spécifiée (dans ce cas, définissant la this
valeur sur `Number).Number.call
) est l'index, et le premier est la valeur this.Number
est appelé avec this
as 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 Number
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.
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 undefined
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 le Array
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 rendement undefined
est 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 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:
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 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 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
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.
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é 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] === 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
[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.