Definiert oder undefiniert? Nuancen beim Erstellen von Arrays in JavaScript

Bild

Vor ein paar Monaten bin ich auf eine interessante Frage zum Stackoverflow gestoßen , bei der eine Person, kurz gesagt, eine leere 5x5- Matrix erstellen wollte und mit einer Methode erfolgreich war, mit der anderen jedoch nicht. In der anschließenden Diskussion wurden interessante Gedanken zu diesem Thema zitiert.

Die Person, die die Frage gestellt hat, sowie diejenigen, die sie beantwortet haben, haben zwar nicht darauf geachtet, dass die Matrix tatsächlich nicht erstellt werden konnte und das Ergebnis der Berechnung falsch war. All dies interessierte mich und ich beschloss, etwas tiefer zu graben, um dann zu interessanten Schlussfolgerungen zu kommen, die ich jetzt mit Ihnen teilen werde.

Hinweis: Ich habe auch unter dieser Diskussion unter dem Spitznamen AndreyGS geantwortet - dort habe ich ziemlich kurz geantwortet, hier werde ich versuchen, das Problem vollständig zu behandeln.

Im Allgemeinen besteht die Herausforderung darin, ein Array zu erstellen. Wie machen wir das? Seltsamerweise gibt es verschiedene Optionen, je nachdem, was wir bekommen möchten.

Wir wissen, dass Funktionen in JavaScript zwei interne Methoden haben, Call und Construct . Wenn wir das neue Schlüsselwort verwenden, wird die Construct-Methode verwendet, die eine neue Instanz des Objekts erstellt, ihm diesen Verweis zuweist und dann den Hauptteil der Funktion ausführt. Nicht alle Funktionen haben diese Methode, aber für uns ist dies momentan nicht so wichtig.

Beim Erstellen von Arrays gibt es eine Besonderheit: Es spielt keine Rolle, ob wir Array (...) oder neues Array (...) verwenden - die ECMAScript- Spezifikation unterscheidet nicht zwischen ihnen und betrachtet sie außerdem als gleichwertig.

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. 

Daher werde ich nicht schelmisch philosophieren, und in den Beispielen werde ich nur das neue Array (...) -Konstrukt verwenden, um niemanden zu verwirren.

Fangen wir an.

Erstellen Sie ein Array:

 let arr = new Array(5); 

Was haben wir bekommen?

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

Hmm ... nun, im Prinzip sollte es so sein - wir haben die Länge eingestellt und fünf leere Zellen mit dem undefinierten Wert erhalten, an denen weiter gearbeitet werden kann, oder? Es stimmt, es gibt ein paar Punkte, die mich verwirren. Lass es uns überprüfen.

 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 

Wie kommt es, dass wir schließlich eine Matrix bekommen mussten und in jeder Zelle dementsprechend ein Array von 5 Elementen vorhanden sein sollte ...

Wenden wir uns noch einmal der ECMAScript- Dokumentation zu und sehen, was darin über die Methode zum Erstellen von Arrays mit einem Argument geschrieben ist:

 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. 

Und wie wir sehen, stellt sich heraus, dass das Objekt erstellt wurde, die Eigenschaft length in der ArrayCreate-Prozedur erstellt wurde (Punkt 6), der Wert in der Eigenschaft length festgelegt wurde (Punkt 9) und was ist mit den Zellen? Abgesehen von dem Sonderfall, dass das übergebene Argument keine Zahl ist und ein Array mit einer einzelnen Zelle „0“ mit dem entsprechenden Wert (Punkt 7) erstellt wird, gibt es kein Wort darüber ... Das heißt, Länge == 5 ist, aber es gibt keine fünf Zellen. Ja, der Compiler verwirrt uns, wenn wir versuchen, auf eine einzelne Zelle zuzugreifen. Er gibt an, dass ihr Wert undefiniert ist , während dies tatsächlich nicht der Fall ist.

Zum Vergleich hier die Methode zum Erstellen von Arrays mit mehreren Argumenten, die an den Konstruktor gesendet werden:

 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. 

Hier bitte - 10 Punkte, die Schaffung der gleichen Zellen.

Und was macht Array.prototype.map () als nächstes?

 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. 

Klausel 7 - Eine Kopie des ursprünglichen Arrays wird erstellt, in Klausel 10 werden Iterationen für seine Elemente durchgeführt, und insbesondere in Klausel 10.2 wird geprüft, ob sich eine bestimmte Zelle im Quellarray befindet, sodass bei Erfolg Map (10.4) und Erstellen Sie die entsprechende Zelle in der Kopie - 10.4.5. Da 10.2 für jeden der 5 Durchgänge false angibt, wird auch keine einzelne Zelle in der zurückgegebenen Kopie des Arrays erstellt.

Wie der Array-Konstruktor und die Methode Array.prototype.map () funktionieren, haben wir herausgefunden, aber die Aufgabe blieb wie zuvor ungelöst, da die Matrix nicht erstellt wurde. Function.prototype.apply () wird zur Rettung kommen!
Lassen Sie es uns gleich in Aktion überprüfen:

 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 } 

Hurra, alle fünf Zellen sind hier deutlich zu beobachten, und auch die erste Testzelle mit der Nummer „0“ hat einen Deskriptor.

In diesem Fall funktionierte das Programm wie folgt:

  1. Wir haben die Function.prototype.apply () -Methode aufgerufen und den Nullkontext an sie übergeben und als Array new Array (5) .
  2. new Array (5) hat ein Array ohne Zellen erstellt, jedoch mit einer Länge von 5 .
  3. Function.prototype.apply () verwendete die interne Methode zum Aufteilen des Arrays in separate Argumente. Als Ergebnis wurden fünf Argumente mit undefinierten Werten an den Array- Konstruktor übergeben.
  4. Array erhielt 5 Argumente mit undefinierten Werten und fügte sie den entsprechenden Zellen hinzu.

Alles scheint verständlich zu sein, außer was ist diese interne Methode von Function.prototype.apply () , die 5 Argumente aus dem Nichts macht - ich schlage noch einmal vor, die ECMAScript- Dokumentation zu betrachten:

 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. 

Wir betrachten die interessantesten Punkte:

19.2.3.1 - Absatz 3: Erstellen einer Liste von Argumenten aus einem Objekt ähnlich einem Array (wie wir uns erinnern, sollten solche Objekte eine Längeneigenschaft haben).

7.3.17 - die Methode zur Listenerstellung selbst. Es prüft, ob das Objekt ist oder nicht, und wenn ja, eine Anfrage für das Längenfeld (Absatz 4). Dann wird ein Index gleich „0“ erstellt (Absatz 7). Eine Schleife wird mit einem Inkrement des Index auf den Wert aus dem Längenfeld erstellt (Absatz 8). In diesem Zyklus beziehen wir uns auf die Zellenwerte des übertragenen Arrays mit den entsprechenden Indizes (Abschnitte 8a und 8b). Und wie wir uns erinnern, gibt es beim Zugriff auf den Wert einer einzelnen Zelle in einem Array, in dem es tatsächlich keine Zellen gibt, immer noch einen undefinierten Wert. Der resultierende Wert wird am Ende der Argumentliste hinzugefügt (Absatz 8e).

Nun, da alles zusammengepasst hat, können Sie die sehr leere Matrix sicher erstellen.

 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 } 

Wie Sie sehen, konvergiert nun alles und sieht ziemlich einfach aus: Wir erstellen, wie wir bereits wissen, ein einfaches leeres Array Array.apply (null, neues Array (5)) und übergeben es dann an die Map-Methode, die dasselbe Array in erstellt jede der Zellen.

Darüber hinaus können Sie es noch einfacher machen. Der Operator verbreiten - ... erschien in ECMAScript6 und funktioniert, was typisch ist, auch speziell mit Arrays. Deshalb können wir einfach einfahren:

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

oder wir werden es komplett vereinfachen, obwohl ich zuvor versprochen habe, nichts Neues zu berühren ...

 let arr = Array(...Array(5)).map(() => Array(...Array(5))); 
Hinweis: Hier haben wir auch Pfeilfunktionen verwendet, da es sich immer noch um einen Spread-Operator handelt, der in derselben Spezifikation wie sie vorkommt.

Wir werden nicht auf das Prinzip des Spread- Operators eingehen, aber für die allgemeine Entwicklung glaube ich, dass dieses Beispiel auch nützlich war.

Darüber hinaus können wir natürlich unsere eigenen Funktionen erstellen, die mithilfe der Sortierung Function.prototype.apply () normale Arrays mit leeren Zellen für uns erstellen. Dabei werden jedoch die internen Prinzipien von JavaScript und dementsprechend die korrekte und angemessene Verwendung verstanden eingebaute Funktionen sind eine Basis für das Mastering, die Priorität hat. Nun, und natürlich ist es so einfach, schneller und bequemer.

Und schließlich, zurück zu dieser Frage zum Stapelüberlauf - dort, wie ich mich erinnere, hat die Person fälschlicherweise angenommen, dass die Methode, die sie erhalten hat, zur richtigen Antwort geführt hat und dass sie jedoch eine 5x5- Matrix erhalten hat -, hat sich dort ein kleiner Fehler eingeschlichen.

Er fuhr hinein:

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


Was denkst du, wird das tatsächliche Ergebnis hier sein?

Die Antwort
console.log (arr); // Array (5) [(5) [...], (5) [...], (5) [...], (5) [...], (5) [...]]
console.log (arr [0]); // Array (5) [<5 leere Slots>]
console.log (Object.getOwnPropertyDescriptor (arr, "0")); // Objekt {Wert: (5) [...], beschreibbar: wahr, aufzählbar: wahr, konfigurierbar: wahr}
console.log (arr [0] [0]); // undefiniert
console.log (Object.getOwnPropertyDescriptor (arr [0], "0")); // undefiniert

ist es nicht, das ist nicht ganz das, was er wollte ...

Referenzen:

ECMAScript 2015-Sprachspezifikation
Was macht Array.apply eigentlich?

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


All Articles