Der Daniel: Was zum Geier hab ich da aufgeführt? Javascript Objekte und andere Irrsinnigkeiten...

Hallo,

Ok... eigentlich suchte ich eine Shorthand Methode um ein Objekt in einem Objekt zu realisieren, nach dem Muster

// AUSGABE 1
/*
GLOBAL_VARS = {
	einObj: {i: 0}
}
*/

Wollte folgenden Code

// BSP 1
const GLOBAL_VARS = {};
if (!GLOBAL_VARS.einObj) {
	GLOBAL_VARS.einObj = {};
}
GLOBAL_VARS.einObj.i = 0;

mit

// BSP 2
const GLOBAL_VARS = {};
GLOBAL_VARS.einObj.i = 0;

abkürzen, was natürlich nicht funktionierte, da in BSP 2 einObj ja noch nicht definiert wurde und daher einen Fehler auswirft.

Wirklich spannend wurde es, als ich folgende Methode probierte:

// BSP 3
GLOBAL_VARS.einObj = {}.i = 0;

...was mit GLOBAL_VARS = {einObj: 0} quittiert wurde - was zum Geier geht da vor?

Und gibt's da eine Methode um das etwas klobige if-Statement in BSP 1 eleganter zu umschiffen?

LG Daniel

  1. Hallo Daniel,

    wenn es denn unbedingt ein GLOBAL_VARS Objekt sein muss... es gibt andere Methoden (Module).

    Wie auch immer. Das Pattern ist eigentlich dieses:

    window.GLOBAL_VARS = window.GLOBAL_VARS || {};
    GLOBAL_VARS.einObj = GLOBAL_VARS.einObj || {};
    
    GLOBAL_VARS.einObj.i = 47;
    

    GLOBAL_VARS || {} - was heißt das? Schau ins Wiki unter "Was ist Wahrheit".

    Wenn GLOBAL_VARS noch undefiniert ist, findet sich darin der Wert undefined. Dieser Wert ist falsy, deswegen wertet der || Operator auch noch seine rechte Seite aus und liefert diesen Wert zurück. Mehr dazu im Wiki unter Wahrheitstabellen und Kurzschlussreaktionen.

    Warum window.GLOBAL_VARS? Weil Du sonst eine Fehlermeldung bekommen kannst, dass es diese Variable nicht gibt. Insbesondere im strict mode. Und var GLOBAL_VARS kannst Du ja nicht schreiben, wenn Du nicht weißt, ob die Variable schon da ist.

    Dieses Hantieren mit || ist eine Spezialität von JavaScript. Das Ergebnis von || und && ist nicht true oder false, sondern der Wert des linken oder rechten Operanden. Und das nutzt man im oben gezeigten Pattern aus.

    1. Ist GLOBAL_VARS schon da, bleibt es, wie es ist. Ist es noch nicht da, wird es mit einem leeren Objektliteral initialisiert.
    2. Ist GLOBAL_VARS.einObj schon da, blebt es wie es ist. Ist es noch nicht da, wird es mit einem leeren Objektliteral initialisiert.

    Und was ist mit deinem Schlussbeispiel?

    GLOBAL_VARS.einObj = {}.i = 0;

    Böse ist das. Böse und Gemein!

    {} ist ein Objektliteral und erzeugt ein leeres Objekt, mitten im großen Datenhaufen (a.k.a. Heap). Ein Verweis darauf wird ganz oben auf den Wertestapel gelegt. {}.i=0 erzeugt in diesem leeren Objekt ein Property i und speichert darin den Wert 0. Nun kommt das Erbe der 1960er und der Sprache C - der Operator = ist ein Operator wie alle anderen auch, relativ niedrig priorisiert und rechts-assoziativ. Rechts-assoziativ heißt, dass c = b = a wie c = (b = a) zu lesen ist, und nicht wie (c = b) = a (was in JavaScript eh Blödsinn wäre, aber in C++... oh je!). In deinem Fall wird also zuerst {}.i=0 ausgeführt. Zum Erbe gehört auch dies: Jeder Operator Hat Ein Ergebnis. Auch der Zuweisungsoperator. Der Wert von {}.i = 0 ist der zugewiesene Wert, also 0. Dieser Wert ersetzt den Verweis auf das vorhin angelegte Objekt, und wird dann an GLOBAL_VARS.einObj zugewiesen. Danach ist das Statement zu Ende. Dieses kleine Objekt, dem Du eine Eigenschaft i verpasst hast, hat nun keinen mehr, der sich für es interessiert. Und so schwimmt es einsam und traurig irgendwo im Heap herum, ohne eine Rettungsleine, bis der gierige Garbage Collector vorbeischwimmt und es verschlingt. Armes Objekt 😢.

    Anders gesagt:

    GLOBAL_VARS.einObj = 0;

    tut genau das gleiche, ohne ein Waisenobjekt zu hinterlassen.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. WOW - Danke für die Abhandlung

      ...ja ja, die liebe Not mit globalen Variablen...

      Problem ist eben, dass wenn mehrere Funktionen z.B. ein und denselben Index in- oder dekrementieren, dies ohne globaler Zuflucht schlicht nicht möglich ist. Diese globaler Index muss dann in der Funktion ärgerlicherweise sogar per hard code eingefügt werden und kann nicht als Parameter übergeben werden

      let index = 0;
      
      function incrementIndex(i) {
      	return i++;
      }
      incrementIndex(index); // globaler index immer noch 0 => ;( rührt index außerhalb von function scope nicht an
      
      function incrementIndexHardCoded() {
      	return index++;
      }
      incrementIndexHardCoded(); // globaler index nun 1 => :) bezieht sich auf index außerhalb von function scope 
      

      Damit diese nicht massenweise den Namespace zu verschmutzen, keimte in mir eben die Idee, alle globalen Variablen in EINEM globalen Objekt zusammenzufassen, eben GLOBAL_VARS.

      Bin für bessere Ideen aber jederzeit offen! 😀


      ...und was mit:

      GLOBAL_VARS.einObj = {}.i = 0;

      unter der Haube passiert, habe ich zugegeben nicht ganz verstanden. Rechts assoziativ wird diese Zauberformel umgewandelt zu

      GLOBAL_VARS.einObj = {i = 0};

      ? und wie ginge es dann weiter?

      1. Hallo,

        ...und was mit:

        GLOBAL_VARS.einObj = {}.i = 0;

        unter der Haube passiert, habe ich zugegeben nicht ganz verstanden. Rechts assoziativ wird diese Zauberformel umgewandelt zu

        GLOBAL_VARS.einObj = {i = 0};

        nein, die geschweiften Klammern wären so syntaktisch falsch. Man kann aber tatsächlich (runde!) Klammern setzen, um es etwas deutlicher zu machen:

        GLOBAL_VARS.einObj = ({}.i = 0);

        Zuerst wird der Ausdruck in der Klammer ausgewertet. Also ein leeres Objekt erzeugt, dem eine Eigenschaft i angehängt und dieser der Wert 0 zugewiesen. Der Zuweisungs-Operator hat aber auch einen Rückgabewert, und der ist relevant, wenn die Zuweisung innerhalb eines Ausdrucks vorkommt, wie hier. Der Rückgabewert des Zuweisungs-Operators ist der zugewiesene Wert, also 0. Dieser Wert 0 wird nun an GLOBAL_VARS.einObj zugewiesen, und das eben erzeugte Objekt fällt auf den Boden und zerschellt, weil es keinen Namen hat, mit dem man es ansprechen könnte.

        Live long and pros healthy,
         Martin

        --
        Klein φ macht auch Mist.
        1. Hallo Martin,

          fällt auf den Boden und zerschellt

          ts, meinst Du denn der Garbage Collector frißt was vom Boden? Tatsächlich bleibt das Objekt unverändert auf dem Heap vorhanden - es gibt aber keine Referenz mehr darauf. Und das stellt der GC fest, wenn er vorbeikommt, und frisst es auf.

          Wie er das feststellt, ist dagegen kaum erklärbar. Effiziente Garbage Collection ist arkane Kunst, und wird von den Runtimeherstellern eifersüchtig gehütet.

          Rolf

          --
          sumpsi - posui - obstruxi
      2. Hallo Daniel,

        Bin für bessere Ideen aber jederzeit offen!

        Für bessere Ideen müsste man mehr über dein Gesamtkunstwerk wissen.

        Wenn Du Eventhandler mit onclick oder onsonstwas definierst, wirst Du kaum eine Chance haben. Denn die brauchen globale Funktionen und die müssen dann globale Objekte verwenden.

        Aber wenn Du ordentlich mit unobtrusive Script arbeitest, d.h. deine Eventhandler mit addEventListener registrierst, kannst Du Modularisierung mit Hilfe einer IIFE betreiben. Oder Du stellst das Ganze in ein <script type="module">...</script> Element (ECMAScript-Modul, nicht im Internet Explorer verfügbar) und brauchst Dir keine Gedanken um globalen Müll zu machen, denn globale Variablen eines Moduls bleiben im Modul. Zu IIFEs haben wir Artikel im Wiki, zu ECMAScript-Modulen nicht. Dafür hatte ich bisher keine Zeit. Aber das Wilde Weite Web hat viele Infos dazu.

        Das Problem mit dem Hardcoding kannst Du nur über Objekte lösen, denn eine Übergabe per Referenz kennt JavaScript nicht. Aber ein Objekt hast Du ja nun.

        D.h. wenn Du GLOBAL_VAR.foo.bar.i inkrementieren willst, könntest Du eine Funktion "erhöhe" schreiben, die so aussieht:

        function verändere(obj, eigenschaft, wert) {
           obj[eigenschaft] = obj[eigenschaft] + wert;
        }
        
        // Inkrementieren von i mit
        verändere(GLOBAL_VAR.foo.bar, "i", +1);
        // Dekrementieren von i mit
        verändere(GLOBAL_VAR.foo.bar, "i", -1);
        

        Aber schick ist das nicht wirklich.

        Du könntest es auch so machen, dass Du ein Zählerobjekt mit Methoden erstellst. Das folgende ist "modernes" JavaScript; wenn Du auch den Internet Explorer unterstützen willst, musst Du auf die alte Syntax mit Konstruktorfunktion und Prototyp zurückfallen.

        // Einmalig an geeigneter Stelle
        class Counter {
           constructor(startwert) {
              this.value = startwert;
           }
           increment() { this.value++; }
           decrement() { this.value--; }
        }
        
        // Verwendung
        GLOBAL_VAR.foo.bar.zähler = new Counter(0);
        GLOBAL_VAR.foo.bar.zähler.increment();         // Direkt inkrementieren
        console.log(GLOBAL_VAR.foo.bar.zähler.value);  // 1
        erhöhe(GLOBAL_VAR.foo.bar.zähler);             // Aufgabe delegieren
        console.log(GLOBAL_VAR.foo.bar.zähler.value);  // 2
        
        function erhöhe(z) {
           z.increment();
        }
        

        Rolf

        --
        sumpsi - posui - obstruxi
      3. Moin,

        Problem ist eben, dass wenn mehrere Funktionen z.B. ein und denselben Index in- oder dekrementieren, dies ohne globaler Zuflucht schlicht nicht möglich ist.

        Globale Zuflucht? Was soll das denn sein?

        ...und was mit:

        GLOBAL_VARS.einObj = {}.i = 0;

        unter der Haube passiert, habe ich zugegeben nicht ganz verstanden. Rechts assoziativ wird diese Zauberformel umgewandelt zu

        GLOBAL_VARS.einObj = {i = 0};

        ? und wie ginge es dann weiter?

        GLOBAL_VARS.einObj = {}.i = 0;
        // führt zu:
        GLOBAL_VARS.einObj = 0;
        
        GLOBAL_VARS.einObj = {i = 0};
        // Syntax Error
        
        GLOBAL_VARS.einObj = {i: 0};
        // führt zu
        GLOBAL_VARS.einObj.i = 0;
        

        Viele Grüße
        Robert