JavaScript Suche Mentoring, Code Review bei meinem erstem JS Projekt.

C

charly_

Gast
Servus,
ich habe zum üben ein 2D Browsergame in Vanilla JS (ohne libraries) geschrieben und würde mich über Feedback freuen. (Nicht zum Spiel selbst, sondern zum code.)

https://github.com/Karl1b/Sassy-Racer.git

Da ich Autodidakt bin habe ich sicherlich einige Fehler gemacht.

Ich würde mich sehr über konstruktive Kritik von guten Programmierern hier freuen. Wie ist meine Code Struktur? Habe ich iwelche groben Fehler gemacht. die man als erfahrener Programmierer sieht, obwohl erstmal alles gut läuft? Gerne eine Skizze wie die Struktur besser wäre...

Klar finde ich auch Tipps in Richtung "Syntax-Zucker" gut, aber darauf habe ich eh nicht so geachtet, will mir erstmal einen Überblick über alle üblichen Sprachen verschaffen.

LG
 
Ich habe nur mal grob drüber geschaut. Bin auch kein JS-Experte.
Aber was mir auffällt ist, dass du in Formeln sehr häufig konkrete Werte anstatt Konstanten nimmst, z.B. öfter 0.05 und das nicht nur innerhalb einer Datei, sondern auch in verschiedenen.
0.95 ist dann z.B. wohl 1 - 0.05 und könnte auch einmalig daraus berechnet werden.

Es ist besser, solchen Werten Namen zu geben, heute weißt du noch die Bedeutung, aber in ein paar Monaten? Und gerade wenn manchmal Werte nur zufällig gleich sind, wird es schnell unübersichtlich.

Also z.B. accelerationFactor = 0.05 oder so (ob der Name sinnvoll für dich ist kann ich nicht sagen, so genau habe ich es nicht angeschaut).


Und ich finde if-Anweisungen häufiger schlechter zu lesen als sowas:
Code:
        if (this.position.w >= this.position.wmax) {
            this.position.w = this.position.wmax;
        }
        if (this.position.w <= -this.position.wmax) {
            this.position.w = -this.position.wmax;
        }
vs
Code:
            this.position.w = Math.min(this.position.w, this.position.wmax);
            this.position.w = Math.max(this.position.w, -this.position.wmax);
Also bei unterem sehe ich halt, was der Sinn der Anweisung ist - aber ich kann verstehen, wenn es für manche gewöhnungsbedürftig ist.
 
  • Gefällt mir
Reaktionen: charly_
Javascript:
let sec = (this.timer.time / 1000) % 60;
let min = ((this.timer.time / 1000) - (this.timer.time / 1000) % 60) / 60;
Vermeide redundanten Code, insbesondere bei Berechnungen. In diesem Beispiel ist es natürlich überschaubar, aber du teilst hier 3x time durch 1000, um aus deinem Timer, der wohl Millisekunden zählt, die Gesamtsekunden zu errechnen und das ist jedes Mal eine potentielle Fehlerquelle bzw erschwert die spätere Wartung. Ja, es geht in diesem konkreten Fall um Zeitberechnung, die sich nicht ändern wird, aber es geht mir eher um das Prinzip. Wiederkehrende Teilergebnisse, o.ä. sollte man stets in einer Variable kapseln, um sie in der Formel überall dort zu verwenden wo man sie benötigt. Ändert sich die Berechnung eines Teilergebnisses, ändert man es genau an einer Stelle, bei seiner einmaligen Zuweisung. Würde sich ungeachtet der Art der Berechnung in diesem Beispiel der Divisor 1000 ändern, müsstest du ihn an drei Stellen ändern und evtl gibt es noch eine vierte Stelle, die du übersehen hast.

Immer dann, wenn man merkt, dass man dasselbe zum zweiten, dritten, x-ten Mal tippt, sollte man sich fragen ob sich das besser kapseln lässt, zB als Variable totalSeconds o.ä. In diesem kleinen Beispiel vielleicht nicht so ausschlaggebend, aber dennoch mindestens etwas für den Hinterkopf ;)

Javascript:
this.position.x - this.gameWidth * 0.5
this.position.y - this.gameHeight * 0.5
So oder so ähnlich findet man überall im Code Koordinatenberechnungen. Es wäre auch hier zu überlegen, sowas zu kapseln und zB in den Tools in einer Funktion zu berechnen. Ggfs x und y als Koordinaten-Objekt handhaben und dann einfach sowas wie GetGamePosition( objectposition ) aufrufen. Weniger x-y Gefummel und weniger fehleranfällig (zB x und y vertippen). Auch würde man damit den Faktor 0.5 leichter anpassen können, weil er genau an einer Stelle vorkommt - in der Funktion für die Koordinatenberechnung - statt an Dutzenden Stellen quer über den Code verteilt. Das schlägt in dieselbe Kerbe, die @tollertyp schon gehauen hat.


Javascript:
switch (event.code) {
   case "ArrowUp":
      [..]
      break;
}
In diesem Fall kommt der String zwar aus einem definierten Event, aber bei switch case mit Strings sollte man grundsätzlich darauf achten, dass zB "arrowUp" den case nicht triggern würde. Das kann zu ätzenden Fehler führen. Wenn nicht auszuschließen ist, dass der String auch abweichende Großschreibung haben kann - zB eine Benutzereingabe - sollte man den String im switch Statement zB mit toUpperCase() normieren und alle cases entsprechend in Großbuchstaben schreiben. Ich mache das grundsätzlich so, auch wenn ich 100%ig weiß wie die Strings reinkommen. Macht man es immer so, minimiert das das Risiko, dass es dieses eine Mal, wo man es sich sparen wollte, knallt.

Abgesehen davon ist es gewissermaßen ein ungeschriebenes Gesetz, stets den default case anzugeben, selbst wenn er abgesehen vom break nichts enthält. Einige Programmierungebungen tracken das auch und meckern einen fehlenden default case an, weil im Falle eines unerwarteten Werts der switch-Variable ein undefinierter Zustand entstehen kann.


Javascript:
enter() {
   if (this.MENUSTATE == MENUSTATE.START1) {
      this.game.start();
      return;
   }
   if (this.MENUSTATE == MENUSTATE.HIGHSCORE) {
      this.MENUSTATE = MENUSTATE.HIGHSCORESCREEN;
      return;
   }
   if (this.MENUSTATE == MENUSTATE.WINSCREEN) {
      this.MENUSTATE = MENUSTATE.HIGHSCORE;
      return;
   }
   if (this.MENUSTATE == MENUSTATE.HIGHSCORESCREEN) {
      this.MENUSTATE = MENUSTATE.HIGHSCORE;
      return;
   }
   if (this.MENUSTATE == MENUSTATE.GAMEOVERSCREEN) {
      this.MENUSTATE = MENUSTATE.HIGHSCORE;
      return;
   }
}
Es mag persönliche Präferenz sein, aber ich vermeide es, Funktionen mit mehreren return-Punkten zu schreiben, weil ich es für unübersichtlich halte. Mein Prof hat damals gesagt, dass eine Funktion am Ende returned und nicht mittendrin. Wenn überhaupt, mache ich sowas nur als shortcut ganz am Anfang, wenn eine komplexe Funktion durch eine nicht erfüllte Vorbedingung gar nicht erst loslegen muss. Aber selbst dann kapsle ich den Inhalt der Funktion meistens in ein if und die Funktion läuft im Zweifelsfalle eben "leer" zum Ende. Entweder man macht aus den ifs ein if-else if- else Konstrukt oder ein switch und weist jeweils nur den MENUSTATE zu und nach dem if-else bzw switch kommt das return (explizit oder implizit). Ein Eingang und ei Ausgang anstelle von 6 Ausgängen. Auch hier wieder: Im kleinen Beispiel mag das halb so wild sein, aber wenn man sich das angewöhnt und auch bei komplexen Funktionen macht, schießt man sich eher früher als später ins Knie.

Javascript:
update(dt) {
   if (!(this.isRunning)) { return; }
   this.time += dt;
}
Hier zum Beispiel. Wieder 2 returns, das explizite return im if und das implizite return am Ende. Warum nicht so:
Javascript:
update(dt) {
   if (this.isRunning) {
      this.time += dt;
   }
}
Das ist in meinen Augen auch leichter zu lesen. Wenn isRunning, mach was, wenn nicht, mach eben nichts.

Sicherlich gibt es einige, die in diesen Fällen anderer Meinung sind und das ist auch vollkommen legitim, da das meiste eher Fragen des persönlichen Stils sind.
Ansonsten sieht das schon ziemlich gut aus, wenngleich ich selbst eher selten javascript programmiere und das Projekt auch nur auf dem Tablet überflogen habe - keine Ahnung ob das überhaupt funktioniert ;)
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: charly_
Raijin schrieb:
Es mag persönliche Präferenz sein, aber ich vermeide es, Funktionen mit mehreren return-Punkten zu schreiben, weil ich es für unübersichtlich halte. Mein Prof hat damals gesagt, dass eine Funktion am Ende returned und nicht mittendrin. ;)
Damit nimmst du returns aber ihren Zweck 🙂
Funktionen können auch ohne diese arbeiten, indem sie in Sprachen ungefähr so aufgebaut werden:
Code:
public int MeineFunktion()
x = doSomething;
If x .... then MeineFunktion = 1
If x .... then MeineFunktion = 2
etc.
end MeineFunktion
Dann hat die Funktion auch am Ende einen Rückgabewert.
Den Nachteil in meinem Bsp. - unnötige if Anweisungen abarbeiten zu lassen - löst du durch geschicktere if , elseif, else, case Kombinationen ... kein Thema. Deshalb ja, du und dein Prof, ihr braucht eigentlich kein return 🙂.
Wenn man allerdings if Konstrukte vermeiden oder flach halten möchte, muss man returns "mittendrin" einsetzen. Und in meiner Wahrnehmung ist der Trend Verschachtelungen, wie bei if benötigt, gering zu halten.

Aber nicht falsch verstehen, ich finde das ausführliche Feedback an den TE sehr freundlich und hilfreich.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Raijin, tollertyp und Drexel
Ich z.B. hasse es, einen Rückgabewert am Anfang der Funktion definiert zu haben, um den dann an 13 verschiedenen Stellen zu setzen/zu verändern.

Ich mag es lieber "zustandslos" (eher: immutable), also dass Variablen so weit es geht feste Werte haben. Und da sind mir mehrere returns viel lieber. Aus meiner Sicht macht es den Code übersichtlicher, wenn "Variablen" (die sind dann ja nicht mehr variabel) ihren Wert halt einmal zugewiesen bekommen - wenn das nicht so einfach ist, dann schreit das häufig nach Hilfsmethoden.

Das heißt aber nicht, dass ich dann Funktionen mit 13 returns an verschiedensten Stellen gut finden würde (verschiedensten meint, dass ich hier nicht von Funktionen rede, deren einzige Aufgabe eigentlich ist, einen switch durchzuführen und je nach case dann ein return, aber selbst dafür gibt es mittlerweile ja neben der switch-Anweisung in diversen Sprachen auch den switch-Ausdruck).

Aber das sind halt Stil-Fragen

Wichtig aus meiner Sicht ist aber vor allem, dass man einen Stil einigermaßen konsequent durchzieht. Projektkonventionen haben immer Vorrang vor persönlichen Präferenzen. Mischmasch ist das schlimmste.
 
  • Gefällt mir
Reaktionen: Raijin
Zurück
Oben