Défini ou non défini? Nuances de création de tableaux en JavaScript

image

Il y a quelques mois, je suis tombé sur une question intéressante sur stackoverflow , où, en bref, une personne voulait créer une matrice 5x5 vide, et en utilisant une méthode, il a réussi, mais en utilisant une autre, il n'a pas réussi. Au cours de la discussion qui a suivi, des réflexions intéressantes ont été citées à ce sujet.

Certes, la personne qui a posé la question, ainsi que ceux qui lui ont répondu, n'ont pas prêté attention au fait qu'en fait la matrice ne pouvait pas être créée et le résultat du calcul était incorrect. Tout cela m’intéressait, et j’ai décidé de creuser un peu plus, puis de tirer des conclusions intéressantes, que je vais partager avec vous maintenant.

Remarque: J'ai également répondu dans le cadre de cette discussion, sous le surnom d' AndreyGS - là, j'ai répondu assez brièvement, ici, je vais essayer de couvrir complètement le problème.

En général, le défi est de créer un tableau. Comment allons-nous procéder? Curieusement, il existe différentes options, selon ce que nous voulons obtenir.

Nous savons que les fonctions en JavaScript ont deux méthodes internes, Call et Construct . Si nous utilisons le nouveau mot-clé, la méthode Construct est utilisée, qui crée une nouvelle instance de l'objet, lui attribue cette référence, puis exécute le corps de la fonction. Toutes les fonctions n'ont pas cette méthode, mais pour nous, ce n'est pas si important en ce moment.

Lors de la création de tableaux, il y a une particularité: peu importe que nous utilisions un tableau (...) ou un nouveau tableau (...) - la spécification ECMAScript ne les distingue pas et, de plus, les considère comme équivalents.

22.1.1 The Array Constructor The Array constructor is the %Array% intrinsic object and the initial value of the Array property of the global object. When called as a constructor it creates and initializes a new exotic Array object. When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments. 

Par conséquent, je ne philosopherai pas malicieusement, et dans les exemples, je n'utiliserai que la nouvelle construction Array (...) , afin de ne confondre personne.

Commençons.

Créez un tableau:

 let arr = new Array(5); 

Qu'avons-nous obtenu?

 console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined 

Hmm ... eh bien, en principe, il devrait en être ainsi - nous avons défini la longueur et obtenu cinq cellules vides, avec la valeur non définie , qui peuvent être travaillées davantage, non? Certes, il y a quelques points qui me confondent. Voyons ça.

 let arr = new Array(5).map(function() { return new Array(5); }); console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined console.log(arr[0][0]); // TypeError: arr[0] is undefined 

Comment se fait-il, après tout, que nous ayons dû obtenir une matrice, et dans chaque cellule, en conséquence, il devrait y avoir un tableau de 5 éléments ...

Revenons à la documentation ECMAScript et voyons ce qui y est écrit concernant la méthode de création de tableaux avec un argument:

 22.1.1.2 Array (len) This description applies if and only if the Array constructor is called with exactly one argument. 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs = 1. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(0, proto). 7. If Type(len) is not Number, then 1. Let defineStatus be CreateDataProperty(array, "0", len). 2. Assert: defineStatus is true. 3. Let intLen be 1. 8. Else, 1. Let intLen be ToUint32(len). 2. If intLen ≠ len, throw a RangeError exception. 9. Let setStatus be Set(array, "length", intLen, true). 10. Assert: setStatus is not an abrupt completion. 11. Return array. 

Et, ce que nous voyons, il s'avère que l'objet est créé, la propriété length est créée dans la procédure ArrayCreate (point 6), la valeur de la propriété length est définie (point 9), et qu'en est-il des cellules? Mis à part le cas spécial où l'argument passé n'est pas un nombre et qu'un tableau est créé avec une seule cellule «0» avec la valeur correspondante (point 7), il n'y a pas un mot à leur sujet ... Autrement dit, il y a == 5 longueur, mais il n'y a pas cinq cellules. Oui, le compilateur nous confond lorsque nous essayons d'accéder à une seule cellule, il indique que sa valeur n'est pas définie , alors qu'elle ne l'est pas réellement.

Voici, à titre de comparaison, la méthode de création de tableaux avec plusieurs arguments envoyés au constructeur:

 22.1.1.3 Array (...items ) This description applies if and only if the Array constructor is called with at least two arguments. When the Array function is called the following steps are taken: 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs ≥ 2. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(numberOfArgs, proto). 7. ReturnIfAbrupt(array). 8. Let k be 0. 9. Let items be a zero-origined List containing the argument items in order. 10. Repeat, while k < numberOfArgs 1. Let Pk be ToString(k). 2. Let itemK be items[k]. 3. Let defineStatus be CreateDataProperty(array, Pk, itemK). 4. Assert: defineStatus is true. 5. Increase k by 1. 11. Assert: the value of array's length property is numberOfArgs. 12. Return array. 

Ici, s'il vous plaît - 10 points, la création de ces mêmes cellules.

Et maintenant, que fait Array.prototype.map () ensuite?

 22.1.3.15 Array.prototype.map ( callbackfn [ , thisArg ] ) 1. Let O be ToObject(this value). 2. ReturnIfAbrupt(O). 3. Let len be ToLength(Get(O, "length")). 4. ReturnIfAbrupt(len). 5. If IsCallable(callbackfn) is false, throw a TypeError exception. 6. If thisArg was supplied, let T be thisArg; else let T be undefined. 7. Let A be ArraySpeciesCreate(O, len). 8. ReturnIfAbrupt(A). 9. Let k be 0. 10. Repeat, while k < len 1. Let Pk be ToString(k). 2. Let kPresent be HasProperty(O, Pk). 3. ReturnIfAbrupt(kPresent). 4. If kPresent is true, then 1. Let kValue be Get(O, Pk). 2. ReturnIfAbrupt(kValue). 3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»). 4. ReturnIfAbrupt(mappedValue). 5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue). 6. ReturnIfAbrupt(status). 5. Increase k by 1. 11. Return A. 

Article 7 - une copie du tableau d'origine est créée, à l'article 10, des itérations sont effectuées sur ses éléments, et, en particulier, l'article 10.2 vérifie s'il y a une cellule spécifique dans le tableau source, de sorte qu'en cas de succès, mappez (10.4) et créer la cellule appropriée dans la copie - 10.4.5. Étant donné que 10.2 donne false pour chacune des 5 passes, aucune cellule de la copie retournée du tableau ne sera créée non plus.

Donc, comment le constructeur de tableau et la méthode Array.prototype.map () fonctionnent, nous l'avons compris, mais la tâche est restée comme avant non résolue, car la matrice n'a pas été construite. Function.prototype.apply () viendra à la rescousse!
Voyons tout de suite en action:

 let arr = Array.apply(null, new Array(5)); console.log(arr); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

Hourra, les cinq cellules sont clairement observées ici, et la première cellule de test portant le numéro «0» a également un descripteur.

Dans ce cas, le programme a fonctionné comme suit:

  1. Nous avons appelé la méthode Function.prototype.apply () et lui avons transmis le contexte nul , et en tant que nouveau tableau Array (5) .
  2. new Array (5) a créé un tableau sans cellules, mais avec une longueur de 5 .
  3. Function.prototype.apply () a utilisé la méthode interne de décomposition du tableau en arguments séparés, par conséquent, a transmis cinq arguments avec des valeurs non définies au constructeur Array .
  4. Le tableau a reçu 5 arguments avec des valeurs non définies , les a ajoutés aux cellules correspondantes.

Tout semble clair, sauf quelle est cette méthode interne de Function.prototype.apply () , qui fait 5 arguments à partir de rien - je suggère à nouveau de regarder la documentation ECMAScript :

 19.2.3.1 Function.prototype.apply 1. If IsCallable(func) is false, throw a TypeError exception. 2. If argArray is null or undefined, then Return Call(func, thisArg). 3. Let argList be CreateListFromArrayLike(argArray). 7.3.17 CreateListFromArrayLike (obj [, elementTypes] ) 1. ReturnIfAbrupt(obj). 2. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object). 3. If Type(obj) is not Object, throw a TypeError exception. 4. Let len be ToLength(Get(obj, "length")). 5. ReturnIfAbrupt(len). 6. Let list be an empty List. 7. Let index be 0. 8. Repeat while index < len a. Let indexName be ToString(index). b. Let next be Get(obj, indexName). c. ReturnIfAbrupt(next). d. If Type(next) is not an element of elementTypes, throw a TypeError exception. e. Append next as the last element of list. f. Set index to index + 1. 9. Return list. 

Nous regardons les points les plus intéressants:

19.2.3.1 - paragraphe 3: création d'une liste d'arguments à partir d'un objet similaire à un tableau (comme nous le rappelons, de tels objets devraient avoir une propriété length).

7.3.17 - la méthode de création de liste elle-même. Il vérifie si l'objet est ou non, et si oui, une demande pour le champ de longueur (paragraphe 4). Ensuite, un index égal à «0» est créé (paragraphe 7). Une boucle est créée avec un incrément de l'index à la valeur prise dans le champ de longueur (paragraphe 8). Dans ce cycle, nous nous référons aux valeurs des cellules du tableau transmis avec les indices correspondants (articles 8a et 8b). Et comme nous nous en souvenons, lorsque vous accédez à la valeur d'une seule cellule dans un tableau dans lequel il n'y a en fait pas de cellules, cela donne toujours une valeur - non définie . La valeur résultante est ajoutée à la fin de la liste des arguments (paragraphe 8e).

Eh bien, maintenant que tout est en place, vous pouvez construire en toute sécurité la matrice très vide.

 let arr = Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); }); console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ] console.log(arr[0]); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: (5) […], writable: true, enumerable: true, configurable: true } console.log(arr[0][0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr[0],"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

Maintenant, comme vous pouvez le voir, tout converge et semble assez simple: nous, comme nous le savons déjà, créons un simple tableau Array.apply (null, new Array (5)) vide puis passez-le à la méthode map, qui crée le même tableau dans chacune des cellules.

De plus, vous pouvez le rendre encore plus facile. L'opérateur spread - ... est apparu dans ECMAScript6 , et, ce qui est typique, il fonctionne également spécifiquement avec les tableaux. Par conséquent, nous pouvons simplement entrer:

 let arr = new Array(...new Array(5)).map(() => new Array(...new Array(5))); 

ou nous allons le simplifier complètement, même si j'ai déjà promis à nouveau de ne pas toucher ...

 let arr = Array(...Array(5)).map(() => Array(...Array(5))); 
Remarque: ici, nous avons également utilisé des fonctions de flèche, car nous avons toujours affaire à un opérateur d'étalement qui apparaissait dans la même spécification qu'eux.

Nous n'entrerons pas dans le principe de l'opérateur de spread , cependant, pour le développement général, je crois que cet exemple était également utile.

De plus, nous pouvons bien sûr créer nos fonctions qui, en utilisant le tri Function.prototype.apply () , créeront des tableaux normaux pour nous avec des cellules vides, cependant, en comprenant les principes internes de JavaScript et, en conséquence, l'utilisation correcte et adéquate fonctions intégrées, est une base à maîtriser qui est une priorité. Eh bien, et, bien sûr, c'est si simple, plus rapide et plus pratique.

Et enfin, revenant à cette même question sur stackoverflow - là, je me souviens, la personne a considéré à tort que la méthode qu'il a reçue a conduit à la bonne réponse, et qu'il a reçu une matrice 5x5 , cependant - une petite erreur s'est glissée dedans.

Il est entré:

Array.apply(null, new Array(5)).map(function(){
return new Array(5);
});


Selon vous, quel sera le résultat réel ici?

La réponse
console.log (arr); // Tableau (5) [(5) [...], (5) [...], (5) [...], (5) [...], (5) [...]]
console.log (arr [0]); // Array (5) [<5 emplacements vides>]
console.log (Object.getOwnPropertyDescriptor (arr, "0")); // Objet {valeur: (5) [...], inscriptible: vrai, énumérable: vrai, configurable: vrai}
console.log (arr [0] [0]); // non défini
console.log (Object.getOwnPropertyDescriptor (arr [0], "0")); // non défini

n'est-ce pas, ce n'est pas tout à fait ce qu'il voulait ...

Références:

Spécification de langue ECMAScript 2015
Que fait Array.apply?

Source: https://habr.com/ru/post/fr463041/


All Articles