J'ai rencontré le code suivant dans la liste de diffusion es-discuter:
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?
Comprendre ce "hack" nécessite de comprendre plusieurs choses:
Array(5).map(...)
Function.prototype.apply
Gère les argumentsArray
gère plusieurs argumentsNumber
gère les argumentsFunction.prototype.call
Ce sont des sujets assez avancés en javascript, donc ce sera plus que plutôt long. Nous allons commencer par le haut. Bouclez!
Array(5).map
?Qu'est-ce qu'un tableau, vraiment? Un objet normal, contenant des clés entières, qui mappent sur des valeurs. Il a d'autres fonctionnalités spéciales, par exemple la variable magique length
, mais à la base, c'est une carte key => value
Normale, comme tout 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 mappages key=>value
Du tableau, qui peut être différent de arr.length
.
L'expansion du tableau via arr.length
ne crée pas de nouveaux mappages key=>value
, Ce n'est donc pas que le tableau ait des valeurs indéfinies , il 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 la tête un peu et voir pourquoi des fonctions comme arr.map
Ne parcourent pas ces propriétés. Si arr[3]
Était simplement indéfini et que la clé existait, toutes ces fonctions de tableau passeraient simplement dessus 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 que la clé elle-même n'a jamais été là: L'appel de undefined.toUpperCase
Aurait déclenché 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 les choses. Section 15.4.2.2 décrit le processus. Il y a un tas de mumbo jumbo 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 les spécifications réelles) que len
est un uint32 valide, et pas n'importe quel nombre de valeurs)
Alors maintenant, vous pouvez voir pourquoi faire Array(5).map(...)
ne fonctionnerait pas - nous ne définissons pas les éléments len
sur le tableau, nous ne créons pas les mappages key => value
, Nous modifiez simplement la propriété length
.
Maintenant que nous avons cela à l'écart, regardons la deuxième chose magique:
Function.prototype.apply
FonctionneCe que apply
fait, c'est essentiellement prendre un tableau et le dérouler comme arguments d'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 variable spéciale arguments
:
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 voulu). Le mappage key => value
Peut ne pas avoir existé dans le tableau que nous avons transmis à apply
, mais il existe certainement dans la variable arguments
. 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 Section 15.3.4. , où Function.prototype.apply
Est défini. 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 boucle for
sur length
éléments, créant un list
de valeurs correspondantes (list
est un vaudou interne, mais c'est essentiellement 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 propriété length
. Et maintenant, nous pouvons voir pourquoi les valeurs ne sont pas définies, mais les clés ne le sont pas, sur arguments
: Nous créons les mappages key=>value
.
Ouf, donc cela n'a peut-être pas été plus court que la partie précédente. Mais il y aura du gâteau à la fin, 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 le suivant:
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 argument length
à 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 quoi faire. Le dernier paragraphe est tout ce qui compte pour nous, et il est formulé vraiment bizarrement, 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 non définies, et nous retournons un tableau de ces valeurs non définies.
Enfin, nous pouvons déchiffrer les éléments suivants:
Array.apply(null, { length: 5 })
Nous avons vu qu'il retourne un tableau contenant 5 valeurs indéfinies, avec des clés toutes existantes.
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 dépend pas tellement de hacks obscurs.
Number
traite les entréesFaire Number(something)
( section 15.7.1 ) convertit something
en un nombre, et c'est tout. Comment cela se fait est un peu compliqué, surtout dans le cas des chaînes, mais l'opération est définie dans section 9. au cas où vous seriez intéressé.
Function.prototype.call
call
est le frère de apply
, défini dans section 15.3.4.4 . Au lieu de prendre un tableau d'arguments, il prend simplement les arguments qu'il a reçus et les transmet.
Les choses deviennent intéressantes lorsque vous enchaînez plus d'un call
ensemble, lancez l'étrange jusqu'à 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
C'est assez intéressant jusqu'à ce que vous compreniez ce qui se passe. log.call
Est juste une fonction, équivalente à la méthode call
de toute autre fonction, et en tant que telle, possède également une méthode call
:
log.call === log.call.call; //true
log.call === Function.call; //true
Et que fait call
? Il accepte un thisArg
et un tas d'arguments et appelle sa fonction parent. Nous pouvons le définir via apply
(encore une fois, du 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 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 nous-mêmes un argument this
, il prend par défaut window
. Prenez note de l'ordre dans lequel les arguments sont fournis pour notre rappel, et remontons 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. Que se passe t-il ici? Nous pouvons voir dans section 15.4.4.18 , où forEach
est défini, ce qui se passe à peu près comme suit:
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 passe sur ce tableau et prend ses indices, résultant en un tableau d'indices d'éléments:
[0, 1, 2, 3, 4]
Avertissement : Ceci est une description très formelle du code ci-dessus - voici comment [~ # ~] i [~ # ~] savoir 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 dans votre visage et moins "aha".
Plusieurs choses se produisent ici. Brisons-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 du tableau est appelé en tant que fonction avec Function.prototype.apply
.
this
est null
ce qui n'a pas d'importance pour le constructeur Array (this
est le même this
que dans le contexte selon 15.3.4.3.2 .une.new Array
Est appelé en passant un objet avec une propriété length
- qui fait que cet objet soit un tableau comme pour tout ce qui compte pour .apply
En raison de la clause suivante dans .apply
: .apply
Transmet des arguments de 0 à .length
, Car appeler [[Get]]
Sur { length: 5 }
Avec les valeurs 0 à 4 donne undefined
le constructeur du tableau est appelé avec cinq arguments dont la valeur est undefined
(obtention d'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. Remarque : notez ici la différence entre Array.apply(0,{length: 5})
et Array(5)
, la première créant cinq fois le type de valeur primitif undefined
et ce dernier créant un tableau vide de longueur 5. Plus précisément, en raison du comportement de .map
(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 valeur this
spécifiée (dans ce cas, la configuration la valeur this
à `Number).Number.call
) Est l'index, et le premier est la valeur this.Number
est appelé avec this
comme undefined
(la valeur du tableau) et l'index comme paramètre. Donc, c'est fondamentalement la même chose que de mapper chaque undefined
à son index de tableau (puisque l'appel Number
effectue une conversion de type, dans ce cas d'un nombre à l'autre sans changer 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 de notre code.
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 appelle la fonction map
du tableau qui prend 2 arguments et retourne un nouveau tableau de la même taille.
Le premier argument que map
prend est en fait une fonction à appliquer sur chaque élément du tableau, il devrait ê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 pris par map
est passé à la fonction que vous passez comme premier argument. Mais ce ne serait pas a, b, ni c dans le cas de foo
, ce 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 le rendre 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 d'analyser le deuxième argument en un nombre (je ne suis pas sûr de ce qu'il fait avec le premier argument).
Puisque le deuxième argument que 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
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 fonction map
dans votre code n'a en fait aucun effet sur le résultat. Corrigez-moi si je me trompe, s'il vous plaît.
Un tableau est simplement un objet comprenant le champ `` longueur '' et certaines méthodes (par exemple Push). Alors 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 n'est pas définie (c'est-à-dire arr[0] === undefined
donne vrai).
Comme pour 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 vous reste plus qu'à vous convaincre que le résultat de la fonction de cartographie est l'index. L'astuce consiste à utiliser la méthode nommée 'call' (*) qui invoque une fonction avec la petite exception que le premier paramètre est défini pour être le contexte 'this', et le second devient le premier param (et ainsi de suite). Par coïncidence, lorsque la fonction de mappage est invoquée, le deuxième paramètre est l'index.
Enfin et surtout, la méthode qui est invoquée est la 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