Christian Schubert: Best Practice zur Reinitialisierung von Variablen

Hey ho,

Ich habe eine Abfolge von Funktionen, die hintereinander aufgerufen werden (a.k.a. "Programm" 😎). Dieses Programm wird unter bestimmten Voraussetzungen reinitialisiert und erneut durchlaufen, daher müssen alle Variablen, die sich im global Namespace befinden, zurückgesetzt werden (quasi auf undefined).

Nun habe ich nach bestem Wissen und Gewissen versucht, den global Namespace nicht mit Variablen zu verschmutzen, das scheint aber unter gewissen Voraussetzungen unabdingbar (ruft z.B. Funktion A eine rekursive Timout Funktion auf, welche in Funktion B wieder eingefangen werden muss [clearTimeout()], so muss das Timeout selbst in einer für beide Funktionen zugänglichen Variable gespeichert werden, usw. - von mir aus auch requestAnimationFrame() / cancelAnimationFrame(), etc.).

Definiere ich also zu Beginn let foo, bar, baz; , so muss ich für den erneuten Durchlauf des Programms eine Reinitialisierungsfunktion à la

function initVars() {
	foo = undefined;
	bar = undefined;
	// (...)
	baz = undefined;
}
initVars();

starten. Diese Methode erscheint mir gar barbarisch, gibt's dafür einen eleganteren / besseren / ...ultimativ RICHTIGEN Best Practice Usecase?

Danke für eure Gedanken, Christian.

😏

  1. Hallo,

    Ich habe eine Abfolge von Funktionen, die hintereinander aufgerufen werden (a.k.a. "Programm" 😎). Dieses Programm wird unter bestimmten Voraussetzungen reinitialisiert und erneut durchlaufen, daher müssen alle Variablen, die sich im global Namespace befinden, zurückgesetzt werden (quasi auf undefined).

    warum "undefined"?
    Ich halte es für völlig ausreichend, vor einem Anweisungsblock alle Variablen, die darin verwendet werden, auf den jeweils gewünschten Wert zu setzen. Im Fall der Modularisierung durch Funktionen kann man das Prinzip noch weiter ins Detail treiben: Beim Funktionsaufruf genau die Werte (Ausdrücke) übergeben, die man tatsächlich haben möchte.

    Wenn man prophylaktisch alle beteiligten Variablen auf Defaultwerte setzt (z.B. undefined), ist das IMO eher ein Zeichen für schlechen Programmierstil.

    Live long and pros healthy,
     Martin

    --
    Versuchungen sollte man nachgeben. Wer weiß, ob sie wiederkommen.
    1. warum "undefined"?

      Da eine Variable, bevor ihr ein Wert zugewiesen wird, eben undefined ist.

      Ich halte es für völlig ausreichend, vor einem Anweisungsblock alle Variablen, die darin verwendet werden, auf den jeweils gewünschten Wert zu setzen. Im Fall der Modularisierung durch Funktionen kann man das Prinzip noch weiter ins Detail treiben: Beim Funktionsaufruf genau die Werte (Ausdrücke) übergeben, die man tatsächlich haben möchte.

      Wenn man prophylaktisch alle beteiligten Variablen auf Defaultwerte setzt (z.B. undefined), ist das IMO eher ein Zeichen für schlechen Programmierstil.

      Live long and pros healthy,
       Martin

      Du hast schon recht, ich überanalysiere die Sache wahrscheinlich und schieße mit Spatzen auf Kanonen (oder umgekehrt).

      Prinzipiell wird allen undefinierten Variablen per Funktionsaufruf ja ein Ursprungswert zugewiesen; i.e. bleibt vielleicht irgendwo ein Counter im global Namespace mit einem unerwünschten Wert "übrig" (der sich deswegen dort befindet, weil z.B. verschiedene Teile des Programms unabhängig voneinander darauf Zugriff haben müssen), so wird selbiger Counter bei Aufruf der erstrelevanten Funktion sowieso zurückgesetzt, nach dem Schema

      let einCounter;
      
      function erstrelevanteFunktion() {
      	einCounter = 0; // wird hier wieder zurückgesetzt
      	// etc.
      }
      
      function andereFunktion() {
      	einCounter++;
      	// etc.
      }
      

      Dir noch einen schönen Abend, Christian. ✌️

  2. Hi there,

    Nun habe ich nach bestem Wissen und Gewissen versucht, den global Namespace nicht mit Variablen zu verschmutzen, das scheint aber unter gewissen Voraussetzungen unabdingbar[...]

    Wenn Dir das so wichtig ist, es nicht zu tun (wofür es gute Gründe geben mag) dann erzeuge halt einfach ein Objekt, dem Du Deine "globalen" Variablen zuweist. Das kannst Du dann verschmutzen wie Du willst...

  3. Hallo Christian,

    das scheint aber unter gewissen Voraussetzungen unabdingbar (ruft z.B. Funktion A eine rekursive Timout Funktion auf, welche in Funktion B wieder eingefangen werden muss [clearTimeout()], so muss das Timeout selbst in einer für beide Funktionen zugänglichen Variable gespeichert werden

    In den allermeisten Fällen scheint das aber nur so. Und gerade in deinem Beispielfall ist es nicht so.

    JavaScript bietet mit Closures einige Möglichkeiten, Daten zwischen Funktionen zu teilen, ohne globale Variablen zu benötigen. Dafür verwendet man an Stelle globaler Variablen einen Funktionskontext. Mal ganz strikt vereinfacht:

    function runIt(startButton, stopButton) {
       let timeoutId;
    
       function startIt() {
          startButton.removeEventListener("click", startIt);
          timeoutId = setTimeout(doItEverySecond, 1000);
       }
    
       function stopIt() {
          clearTimeout(timeoutId);
          stopButton.removeEventListener("click", stopIt);
       }
    
       function doItEverySecond() {
          // do something
          timeoutId = setTimeout(doItEverySecond, 1000);
       }
    
       startButton.addEventListener("click", startIt);
       stopButton.addEventListener("click", stopIt);
    }
    
    runIt(btn1, btn2);
    

    Wenn runIt aufgerufen wird, entsteht eine Closure. Diese enthält alle lokalen Variablen der Funktion mit ihren Werten. Im Beispiel sind das sechs: startButton, stopButton, timeoutId, startIt, stopIt und doItEverySecond. Ja, richtig gelesen, die Funktionen sind lokale Daten im runIt Aufruf. Allerdings read-only. Ein wichtiger Aspekt an diesen Funktionsobjekten ist, dass sie eine Referenz auf die Closure enthalten, in der sie entstanden sind.

    Die runIt-Funktion, die ich skizziert habe, bekommt zwei Button-Objekte übergeben und registriert für beide einen click-Handler. Und dann endet sie. Aber weil startIt und stopIt nun als Eventhandler registriert sind, existieren noch Referenzen auf sie. Und weil startIt und stopIt Referenzen auf die runIt-Closure halten, existiert auch die Closure weiter. Und damit auch timeoutId und doItEverySecond.

    Um keinen mehrfachen Start zu produzieren, deregistriert sich die startIt Funktion selbst, sobald man start geklickt hat. Ssst, ein Faden weniger, an dem die Closure hängt.

    D.h. ohne ein einzige globale Variable hängt diese Closure nun an einem einzigen Faden im DOM und tickt vor sich hin. Immer wenn setTimeout gerufen wird, wird doItEverySecond als Timeout-Handler registriert, das ist der zweite Faden, an dem die Closure hängt. Bis zu dem Moment, wo man Stop klickt. Das löscht den Timeout - schnipp - und deregistriert stopIt als click-Handler - schnapp. Beide Fäden sind weg, und bei nächster Gelegenheit wird die Closure vom Garbage Collector abgeräumt.

    Und wenn Du runIt aufrufst, geht es wieder von vorne los.

    Ein besonders schicker Aspekt der Sache ist: All das ist komplett eingekapselt. Keine globale Variable. Absolut keine Möglichkeit, dass sich irgendwelche Dinge gegenseitig überlagern. Du könntest sogar zwei Start- und zwei Stop-Buttons haben. Du rufst runIt zweimal auf, einmal für das eine Start/Stop Pärchen, und noch einmal für das zweite. Und nun ticken zwei Timeouthandler vor sich hin, keiner weiß was vom anderen, keiner hat auch nur die geringste Chance, irgendwie den anderen zu beeinflussen.

    Ich kann es nur so abstrakt erklären, weil Du ja auch nur abstrakt dein Problem vorgestellt hast. Für requestAnimationFrame gilt die Argumentation übrigens analog.

    Wenn Du damit nicht weiterkommst, erzähl mehr über deine Problemstellung. Oder gib uns einen Link auf die Seite, falls sie öffentlich ist, dann können wir uns deinen Code anschauen.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      nur als Ergänzung:

            timeoutId = setTimeout(doItEverySecond, 1000);
      

      hier würde ich setInterval nehmen.

         function startIt() {
            startButton.removeEventListener("click", startIt);
      …
         startButton.addEventListener("click", startIt);
      

      um ein Event nur einmal auszulösen, gibt es die „once“-Option:

        startButton.addEventListener("click", startIt, {once: true});
      

      wird allerdings vom IE11 nicht unterstützt.

      Gruß
      Jürgen

      1. Hallo Jürgen,

        hier würde ich setInterval nehmen.

        du hast natürlich recht, aber es ist eigentlich nicht relevant für die Fragestellung. Auch setInterval muss ich canceln können und dann brauche ich die gleiche Struktur. Und bei requestAnimationFrame gibt's ohnehin kein Interval-Analogon.

        once

        Danke, den hatte ich aus dem Blick verloren.

        IE11

        Den würde ich gerne aus dem Blick verlieren, aber er tanzt immer noch frech vor meiner Nase rum.

        Rolf

        --
        sumpsi - posui - obstruxi
  4. Hallo alle,

    ich habe einige Änderungen an setTimeout vorgenommen und wäre ganz froh, wenn da jemand draufschaut.

    Das Abschlussbeispiel möchte ich noch verändern. setTimeout ist eigentlich gerade NICHT für einen Countdown geeignet, dafür ist setInterval viel besser. Ich überlege noch an einem wirklich guten Einsatz für setTimeout mit einer Wartezeit > 0.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      Ich überlege noch an einem wirklich guten Einsatz für setTimeout mit einer Wartezeit > 0.

      verzogern der Auswertung des Input-Events, damit nicht bei jeder Ziffer reagiert wird:

      var to;
      document.getElementById("rechner").addEventListener("input",function(event) {
        window.clearTimeout(to);
        to = window.setTimeout(rechne,500);
      },false);
      

      Gruß
      Jürgen

      1. Hallo JürgenB,

        so?

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          ja.

          Gruß
          Jürgen

        2. @@Rolf B

          so?

          Nein. Die debounce function ist ein allgemein wiederverwendbares Pattern.

          Also so:

          // debounce aus Bibliothek einfügen oder Code einbinden
          
          document.body.addEventListener("input", recalculate);
             
          function recalculate() {
             debounce(function() {
                let sum = 0;
                for (let numEntry of document.querySelectorAll("input[type=number]")) {
                   if (!isNaN(numEntry.valueAsNumber))
                      sum += numEntry.valueAsNumber;
                }
                document.getElementById("summe").textContent = sum;
             }, 500);
          }
          

          😷 LLAP

          --
          Wenn der Faschismus wiederkehrt, wird er nicht sagen: „Hallo, ich bin der Faschismus.“ Er wird sagen: „Hört auf zu zählen! Ich habe gewonnen!“
          1. Tach!

            so?

            Nein. Die debounce function ist ein allgemein wiederverwendbares Pattern.

            Doch. Der Code von RolfB verwendet dieses Pattern.

            Dein Link zeigt dafür eine separate Funktion mit dem zusätzlichem Parameter immediate, der angibt, ob die Funktion vor oder nach dem Timeout ausgeführt werden soll. Zudem wird der Event-Kontext hineingereicht. Lässt man den immediate-Teil weg, und auch das Context-Handling - was in dem gegebenen Beispiel nicht benötigt wird - kommt man funktional auf den gleichen Code. Damit hat man er Debounce-Pattern verwendet.

            dedlfix.

            1. Hallo dedlfix,

              jein. Zum Pattern gehören zwei Teile: die setTimeout-Logik, und das Einkapseln in eine Funktion, die sich um das debouncing kümmert.

              Das Einkapseln fehlt. Als Fullmultistack Entwickler (kann alles, aber nichts richtig) war mir nicht bekannt, dass es eine Standardimplementierung gibt.

              Die Variante mit immediate ist ein nettes Goodie, aber ich musste tatsächlich erstmal ein bisschen mit der Funktion herumspielen, bis ich verstanden habe, wo genau der Unterschied im Verhalten ist. Ich werde jetzt zwei Dinge tun:

              (1) Im Wiki einen Tutorial-Artikel zur debounce-Funktion bereitstellen, ihre Arbeitsweise erklären und dabei natürlich auf underscore.js als Quelle verweisen. (2) Bei setTimeout auf diesen Artikel referenzieren und die debounce-Funktion zum Einsatz bringen.

              Rolf

              --
              sumpsi - posui - obstruxi
              1. Tach!

                Die Variante mit immediate ist ein nettes Goodie, aber ich musste tatsächlich erstmal ein bisschen mit der Funktion herumspielen, bis ich verstanden habe, wo genau der Unterschied im Verhalten ist.

                Hab ich auch aus dem Code herauszufinden versucht, aber es steht im Fließtext darüber geschrieben.

                Die Implementation gefällt mir nicht richtig. Man hat zwei Stellen, an denen das immediate behandelt wird. Das macht es etwas schwerer verständlich.

                dedlfix.

                1. Hallo dedlfix,

                  Die Implementation gefällt mir nicht richtig.

                  Da bin ich ganz bei Dir, aber was ist die Alternative? Zwei verschiedene Debouncer? Eine Weiche, die je nach Wert von immediate unterschiedliche Funktionen bindet? Der Name `immediate' gefällt mir ohnehin nicht...

                  function debounce(func, wait, useFirstCall = false) {
                  	var timeout, context;
                  	if (onFirstCall) {
                  		return function(...args) {
                  			context = this;
                  			if (!timeout)
                  				func.apply(context, args);
                  			clearTimeout(timeout);
                  			timeout = setTimeout(() => timeout = null, wait);
                  		}
                  	} else {
                  		return function(...args) {
                  			context = this;
                  			clearTimeout(timeout);
                  			timeout = setTimeout(() => {
                  					timeout = null;
                  					func.apply(context, args);
                  				}, wait);
                  		}
                  	}
                  };
                  

                  Das ist jetzt noch ein bisschen modernisiert, mit Lambdas und Rest-Parameter, aber gefallen tut mir das auch nicht. Und mir fehlt auch noch ein Feature: eine Mindestfrequenz, mit der die Funktion aufgerufen wird.

                  Rolf

                  --
                  sumpsi - posui - obstruxi
                  1. Tach!

                    Die Implementation gefällt mir nicht richtig.

                    Da bin ich ganz bei Dir, aber was ist die Alternative?

                    Das weiß ich auch nicht. Wenn ich eine solche Funktionalität benötige, habe ich RxJS zur Hand, da ist das eingebaut.

                    Lass den Teil immediate/useFirstCall einfach weg, den brauchen wir nicht. Es geht hier ja nicht darum einen full-flavored Debouncer vorzustellen.

                    Es gibt da auch noch die Unterscheidung zwischen Throttling und Debouncing. Wir brauchen nur den Debounce-Teil, nicht das Throttling. Besser aufteilen als überladene Funktionen.

                    dedlfix.

                    1. Hallo dedlfix,

                      danke für den Hinweis auf Throttling. Das ist meine Mindestfrequenz 😀

                      Aber doch, eigentlich wollte ich den Debouncer so gut wie möglich darstellen. Ich werde es aber in Bausteinen tun, dann kann jeder überlegen, was er haben will.

                      Rolf

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

                      so, habe mich nun fertig ausgetobt. Bevor ich dafür Beispiel-Frickls mache, hätte ich gerne ein paar Meinungen gehört...

                      https://wiki.selfhtml.org/wiki/JavaScript/Tutorials/Debounce_und_Throttle

                      Rolf

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

                (2) Bei setTimeout auf diesen Artikel referenzieren und die debounce-Funktion zum Einsatz bringen.

                ich würde den einfachen Ansatz stehen lassen und lediglich auf die debounce-Funktion verweisen. Es geht hier ja um ein einfaches Beispiel für den Einsatz von setTimeout.

                Ich habe mir vor einiger Zeit schon mal die debounce-Funktion angesehen und entschieden, dass das für meine Anwendung zu viel ist, und ich sie daher nicht nehme.

                Gruß
                Jürgen

            2. @@dedlfix

              so?

              Nein. Die debounce function ist ein allgemein wiederverwendbares Pattern.

              Doch. Der Code von RolfB verwendet dieses Pattern.

              Aber nicht als wiederverwendbares Pattern.

              Dass die Debounce-Funktion von David Walsh mehr kann als man in diesem Fall benötigt – nun, das ist bei Bibliotheksfunktionen öfter so.

              😷 LLAP

              --
              Wenn der Faschismus wiederkehrt, wird er nicht sagen: „Hallo, ich bin der Faschismus.“ Er wird sagen: „Hört auf zu zählen! Ich habe gewonnen!“
              1. Tach!

                Dass die Debounce-Funktion von David Walsh mehr kann als man in diesem Fall benötigt – nun, das ist bei Bibliotheksfunktionen öfter so.

                Auf der anderen Seite höre ich dein Klagen, dass Bibliotheken böse sind, weil sie immer zu viel Code einbinden, den die armen Anwender unnötigerweise mitladen müssen.

                dedlfix.