Skip to content

Richtlinien für JavaScript und TypeScript

Einleitung

Allen Kapiteln wurde eine eindeutige Nummerierung, der Richtliniennummer, hinzugefügt, um eine eindeutige Identifikation zu ermöglichen. Jede Richtliniennummer besteht aus dem Buchstaben JS(JavaScript oder Typescript) gefolgt von einer Nummer, die den Abschnitt identifiziert. Damit kann eine Regel eindeutig identifiziert werden, z.B. für ein Code-Review.

INFO

Wo notwendig, wird auf die Unterschiede zwischen JavaScript und TypeScript hingewiesen.

Alle Beispiele sind mit 2 Leerzeichen eingerückt, da dies in Markdown die beste Darstellung bietet.

Typescript

Falls es erforderlich ist, wird in Zukunft Typescript in ein eigenes Regeldokument aufgeteilt.

JS1 Allgemeine Regeln

Es gelten die Allgemeinen Regeln für Sprachen.

JS2 Abstraktionsschichten

Zugriffe auf unterliegende Schichten (Vergleich mit GL3 Abstraktionsschichten) sollen in JavaScript vermieden werden.

JS2 Problem

Der direkte Zugriff auf unterliegende Schichten wie Datenbanken, APIs oder andere externe Dienste kann zu einer starken Kopplung und Abhängigkeit führen. Dies ist insbesondere problematisch, wenn der Zugriff direkt in den Anwendungscode (Business-Logik) eingebettet ist und somit eine Mischung von unterschiedlichen Code-Schichten entsteht.

javascript
function onClick() {
    try {
      const connection = this.db.connect();
      const data = connection.query('SELECT * FROM business');

      this.api.sendData(data, { options: "abc;charset=utf-8" });

      document.getElementById('myDiv').innerHTML = data;
    } catch (e) {
      console.error(e);
    }
}

JS2 Lösung

  • Um die Abhängigkeit von unterliegenden Schichten zu reduzieren, sollen Zugriffe auf diese Schichten in separate Klassen oder Module ausgelagert werden.
  • Dependency Injection (DI) soll verwendet werden, um die Abhängigkeiten zwischen den Schichten zu verwalten und den Zugriff auf unterliegende Schichten zu ermöglichen.
  • Die Business-Logik soll von der Implementierungsdetails getrennt werden, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten.
  • Fehlerbehandlung soll in der UI-Schicht über einen allgemeinen Fehlermechanismus erfolgen, um die Business-Logik nicht mit Fehlerbehandlung zu belasten. Nur behandelbare Fehler sollen selbst behandelt werden.
  • Der Einsatz eines Modellansatzes wie MVC, MVP oder MVVM kann helfen, die Business-Logik von der UI zu trennen.
  • Architekturansätze wie Domain-Driven Design (DDD), Model-Driver Architecture (MDA) oder Clean Architecture können ebenfalls helfen, die Abhängigkeiten zu reduzieren und die Business-Logik zu isolieren.
javascript

function onClick() {
  const data = this.dbDi.getBusinessData();

  this.apiDi.sendDataToFoo(data);

  this.uiDi.updateBusinessData(data);
}

// Error handling in UI durch Framework aufgerufen,
// wenn in onClick ein Fehler auftritt
globalErrorHandler = (e) => {
  // Darstellen des Fehlers (so oder ähnlich)
  this.uiDi.showError(e);
}

JS2 Vorteile

  • Simple und klare Struktur in den Methoden/Funktionen
  • Reduzierung der Abhängigkeiten zwischen den Schichten
  • Klare Trennung der Verantwortlichkeiten
  • Verbesserte Wartbarkeit und Erweiterbarkeit des Codes
  • Verbesserte Testbarkeit durch die Möglichkeit, die unterliegenden Schichten zu mocken

JS2 Nachteile

  • Erhöhter Aufwand durch die Notwendigkeit, zusätzliche Klassen oder Module zu erstellen
  • Erhöhter Aufwand durch die Notwendigkeit, Dependency Injection zu verwenden
  • Überblick über die Abhängigkeiten und Struktur des Codes kann schwieriger sein
  • Verhalten zur Laufzeit ist nicht direkt aus dem Code ersichtlich

JS2 Ausnahmen

  • In Prototypen kann der direkte Zugriff auf unterliegende Schichten akzeptabel sein. Der Prototyp muss jedoch nachträglich dahingehend refaktorisiert werden, dass die Zugriffe auf unterliegende Schichten in separate Klassen oder Module ausgelagert werden.
  • In kleinen Anwendungen oder Tools kann der direkte Zugriff auf unterliegende Schichten akzeptabel sein.

JS3 Trennung von Verantwortlichkeiten

Die Trennung von Verantwortlichkeiten (Separation of Concerns) soll für Klassen und Methoden eingehalten werden. Klassen und Methoden sollen nur die Aufgaben erfüllen, für die sie verantwortlich sind. Damit sind Gott- oder Alleskönner-Klassen und -Methoden zu vermeiden.

JS3 Problem

Klassen oder Methoden, die zu viele Aufgaben erfüllen, sind schwer zu verstehen, zu testen, zu warten und gehen oftmals über mehrere Abstraktionsschichten hinweg. Dies führt zu einer erhöhten Kopplung, einer geringeren Kohäsion, einer schlechten Wiederverwendbarkeit, höherer Fehleranfälligkeit und einer schlechteren Lesbarkeit des Codes.

Das folgende Beispiel zeigt eine Business-Service-Klasse, die sowohl Datenbank- als auch API-Zugriffe enthält und direkt auf das UI zugreift. Für Tests müssten mehrere Abhängigkeiten (Datenbank, API, UI) erstellt und injiziert werden, was zu einem komplexen Testaufbau führt.

javascript
class BusinessService {
  constructor(database, api) {
    this.database = database;
    this.api = api;
  }

  onClick() {
    try {
      const connection = this.database.connect();
      const data = connection.query('SELECT * FROM business');

      this.api.sendData(data, { options: "abc;charset=utf-8" });

      document.getElementById('myDiv').innerHTML = data;
    } catch (e) {
      console.error(e);
    }
  }
}
javascript
function godMethod() {
  // Code
  for (let i = 0; i < 10; i++) {
    if (complicatedCondition) {
    } else {
    }
  }

  while (condition) {
    // Code
  }

  if (anotherCondition) {
    // Code
  }

  callAnotherMethod();
}

JS3 Lösung

  • Klassen und Methoden sollen nur eine Aufgabe erfüllen und für eine klare Trennung der Verantwortlichkeiten sorgen.
  • Die Business-Logik soll von der Implementierungsdetails getrennt werden, um eine klare Trennung der Verantwortlichkeiten zu gewährleisten.

Im folgenden Beispiel wird die Business-Logik von der Implementierungsdetails getrennt, indem die Datenbank- und API-Zugriffe in separate Klassen ausgelagert werden.

Als zweites Beispiel wird eine große Methode in kleinere Methoden aufgeteilt, um die Verantwortlichkeiten klarer zu trennen. Dazu werden die Schleifen und Bedingungen in separate innere Methoden ausgelagert.

javascript
class BusinessService {
  constructor(database, api, ui) {
    this.database = database;
    this.api = api;
    this.ui = ui;
  }

  onClick() {
    const data = this.database.getBusinessData();

    this.api.sendDataToFoo(data);

    this.ui.updateBusinessData(data);
  }
}
javascript
function smallMethod() {
  const checkComplicatedCondition = () => {
    for (let i = 0; i < 10; i++) {
      if (complicatedCondition) {
      } else {
      }
    }
  };

  const doWhile = () => {
    while (condition) {
      // Code
    }
  };

  const checkAnotherCondition = () => {
    if (anotherCondition) {
      // Code
    }
  };

  checkComplicatedCondition();

  doWhile();

  checkAnotherCondition();
}
Innere Methoden/Funktionen

In JavaScript könnten komplexe Bereichsblöcke in eigene Methoden einer Klasse ausgelagert werden. Dies führt jedoch zu vielen öffentlichen Methoden führt, die nur von einer Methode aufgerufen werden, und interne Methoden und Information nach aussen sichtbar macht. Das sollte verhindert werden, indem die Funktionen innerhalb der Methode als Konstanten, wie im Beispiel oben, definiert werden.

Alternativ können private Methoden in einer Klasse verwendet werden, um die Verantwortlichkeiten klar zu trennen. Entweder wird eine Methode im Konstruktor definiert oder mit # als private Methode gekennzeichnet, bzw. in TypeScript mit private.

javascript
class MyClass {
  constructor() {
    this.#checkComplicatedCondition();
    this.#doWhile();
    this.#checkAnotherCondition();
  }

  #checkComplicatedCondition() {
    for (let i = 0; i < 10; i++) {
      if (complicatedCondition) {
      } else {
      }
    }
  }

  #doWhile() {
    while (condition) {
      // Code
    }
  }

  #checkAnotherCondition() {
    if (anotherCondition) {
      // Code
    }
  }
}

JS3 Vorteile

  • Einfachere und klarere Struktur in den Methoden/Funktionen
  • Reduzierung der Abhängigkeiten zwischen den Schichten
  • Klare Trennung der Verantwortlichkeiten
  • Verbesserte Wartbarkeit und Erweiterbarkeit des Codes
  • Verbesserte Wiederverwendbarkeit von Klassen und Methoden
  • Verbesserte Testbarkeit durch die Möglichkeit, die unterliegenden Schichten zu mocken

JS3 Nachteile

  • Erhöhter Aufwand durch die Notwendigkeit, zusätzliche Klassen oder Module zu erstellen
  • Überblick über die Abhängigkeiten und Struktur des Codes kann schwieriger sein

Mehr Code durch Trennung

Das Argument "mehr Code" ist kein Argument gegen die Trennung von Verantwortlichkeiten, denn die Trennung führt zu besser wartbarem und testbarem Code, was die Größe des Codes überwiegt. Ein großer Teil des zusätzlichen Codes sind oftmals nur Methoden- oder Klassendeklarationen, die keine zusätzliche Logik enthalten und damit auch nicht getestet werden können (Solche Logik wird vom Compiler/Interpreter/Linter geprüft).

JS4 Trennung von operationalem und integrativem Code

Nach dem Integration Operation Segregation Principle soll Code entweder Operations-Logik oder Integration-Logik enthalten, aber nicht beides.

Operation vs. Integration

Eine Operations-Logik enthält Bedingungen, Schleifen, etc., die die Geschäftsregeln implementieren. Auch API-Aufrufe oder andere I/O-Operationen gehören zur Operations-Logik.

Eine Integration-Logik enthält Code, der andere Code verwendet, um die Operations-Logik zu implementieren.

Eine Hybrid-Logik enthält sowohl Operations- als auch Integrationslogik.

JS4 Problem

Funktionale Abhängigkeiten sin ein Symptom von sich schlecht änderbaren Codes. Durch die Vermischung von Operations- und Integrationslogik wird der Code unübersichtlich und schwer verständlich und lässt sich nur schwer automatisiert testen oder refaktorisieren.

Wenn in Methoden oder Funktionen verhaltenserzeugende Anweisungen (if, while, for, etc.) mit Aufrufen anderer Methoden derselben Codebasis gemischt sind, ist nicht mehr klar erkennbar, wie das Gesamtverhalten entsteht, da viel zu viel in der Methode oder Funktion passiert.

Solche Methoden tendieren oftmals dazu unbegrenzt zu wachsen.

Im folgenden Code besteht keine klare Trennung zwischen Operations- und Integration-Logik. Es wurde einfach die Lösung "herunter-programmiert" und die Logik in einer Methode zusammengefasst. Als Beispiel ist eine kleine Funktion gegeben, die in der Praxis oftmals deutlich größer und komplexer ist.

javascript
function onClick(input) {
  const value = document.getElementById('myInput').value;

  if (!input) {
    return;
  }

  const connection = this.db.connect();
  let data = connection.query('SELECT * FROM Business');

  if (data.foo == 0) {
    data = connection.query('SELECT * FROM FooFoo');
  }

  this.api.sendData(data, { options: "abc;charset=utf-8" });

  document.getElementById('myDiv').innerHTML = data;
}

JS4 Lösung

Die Trennung kann durch die Verwendung von mehreren Zwischenmethoden erreicht werden, die die Operations- und Integrationslogik trennen.

Guard Clause

Strenggenommen ist die Guard Clause eine Operations-Logik, welche die Methode nach IOSP auch zu einer Operations-Logik, statt einer Integration-Logik macht.

Folge dem Prinzip nicht blind

Beachte: Es steht die Lesbarkeit und Verständlichkeit des Codes im Vordergrund!

Wie stark eine Trennung durchgeführt werden soll, wird durch die Größe und Komplexität der Methode bestimmt.

Konzentriere dich daher darauf, die Methoden und Funktionen soweit zu trennen, dass sie leicht verständlich und testbar sind.

Es ist nicht erforderlich, dass jede Methode entweder nur Operations- oder Integrationslogik enthält, nur damit das Prinzip zu 100% eingehalten wird.

javascript

function onClick(input) {
  // Integration-Logik
  // und Trennung von UI und Business-Logik
  const value = document.getElementById('myInput').value;

  this.processData(input);
}

// Hybrid-Logik, wenn guard clause mitgezählt wird
function processData(input) {
  if (!input) {
    // Guard clause, macht die Methode zu einer Operations-Logik
    return;
  }

  // Integration-Logik
  const data = getDataFromBusinessOrFooTable();

  this.sendData(data);
  this.setElementData(data);
}

function getDataFromBusinessTable(connection) {
  // Operation-Logik, da Datenbankabfrage
  return connection.query('SELECT * FROM Business');
}

function getDataFromFooTable(connection) {
  // Operation-Logik, da Datenbankabfrage
  return connection.query('SELECT * FROM FooFoo');
}

// Hybrid-Logik könnte weiter aufgeteilt werden
// aber die Trennung von Operations- und Integration-Logik ist bereits deutlich
function getDataFromBusinessOrFooTable() {
  const connection = this.db.connect();
  const data = this.getDataFromBusinessTable(connection);

  // Beispiel-If-Abfrage
  if (data.foo == 0) {
    return this.getDataFromFooTable(connection);
  }

  return data;
}

function sendData(data) {
  // Operation-Logik, da API-Aufruf
  this.api.sendData(data, { options: "abc;charset=utf-8" });
}

function setElementData(data) {
  // Operation-Logik, da UI-Aufruf
  document.getElementById('myDiv').innerHTML = data;
}

Code-Größe

Im Allgemeinen führt IOSP zu kürzeren Methoden, da die Operations- und Integrationslogik getrennt sind. Jedoch wird insgesamt mehr Code geschrieben, da die Trennung zu mehr Methoden führt, welche neue Zeilen hinzufügen.

JS4 Vorteile

  • Durch die strikte Trennung von Operations- und Integration-Logik wird der Code übersichtlicher und leichter verständlich.
  • Methoden/Funktionen sind einzeln einfacher zu lesen, da sie kurz sind.
  • Methoden/Funktionen sind einzeln einfacher zu testen.
  • Korrektheit von Integrationen lässt sich leicht durch Augenscheinnahme prüfen.
  • Es gibt oftmals eine Haupteinstiegs-Methode, die die Integration-Logik koordiniert und die Operations-Logik in separaten Methoden aufruft.
  • Integrations-Methoden/Funktionen lassen sich leicht erweitern, indem neue Methoden hinzugefügt werden, um neue Anforderungen zu erfüllen.

JS4 Nachteile

  • Die Trennung von Operations- und Integration-Logik kann zu mehr Code führen, da mehr Methoden/Funktionen erstellt werden müssen.

JS4 Ausnahmen

  • In kleinen Anwendungen oder Prototypen kann die Trennung von Operations- und Integration-Logik übertrieben sein.
  • Die strikte Trennung kann in manchen Fällen unnötigen Overhead verursachen (siehe Trennung von getOrCreateUser und getUser).
javascript
class UserService {
  constructor(database) {
    this.database = database;
  }

  // Operation: Benutzer suchen (nur Leseoperation)
  findUser(id) {
    return this.database.find(user => user.id === id);
  }

  // Operation: Benutzer erstellen (nur Schreiboperation)
  createUser(id) {
    const newUser = { id, name: 'New User' };
    this.database.push(newUser);
    return newUser;
  }

  // Operation: Benutzer holen oder erstellen (Logik zur Entscheidung, ob ein Benutzer erstellt werden muss)
  getOrCreateUser(id) {
    const user = this.findUser(id);
    return user ? user : this.createUser(id);
  }

  // Integration: Koordiniert nur den Aufruf von getOrCreateUser
  getUser(id) {
    return this.getOrCreateUser(id);
  }

  // statdessen könnte auch bei dieser einfachen Methode auch ein Hybrid aus Operations- und Integration-Logik verwendet werden
  getUserAlternative(id) {
    let user = this.findUser(id);
    if (!user) {
      user = this.createUser(id);
    }
    return user;
  }
}

JS5 Anwendung von ES6 Features

Mit ES6 stehen viele neue Möglichkeiten zur Verfügung, um den Code zu verbessern. Beispielsweise könnten Pfeilfunktionen, Template-Strings, Default-Parameter, Rest- und Spread-Operator, Destructuring-Zuweisungen, const und let anstelle von var für eine bessere Kontrolle des Scopings, Klassen, Module, Promises und Iteratoren verwendet werden, um den Code kürzer und leichter lesbar zu machen.

DANGER

var darf nicht verwendet werden. Stattdessen sollen const und let verwendet werden, um die Sichtbarkeit von Variablen zu steuern.

JS6 Benennung von Variablen, Funktionen, Klassen und mehr

  • Variablen sind im camelCase camelCase zu benennen: myVariable.
  • Funktionen oder Methoden sind im camelCase zu benennen myFunction() oder myMethod().
  • Klassen sind im PascalCase zu benennen MyClass.
  • Globale Konstanten sind in UPPER_SNAKE_CASE zu benennen MY_CONSTANT.
  • Statische Klassen-Konstanten sind in UPPER_SNAKE_CASE zu benennen MY_CLASS_CONSTANT.
  • Lokale Konstanten können in camelCase sein myConstant oder in UPPER_SNAKE_CASE MY_CONSTANT, wenn sie z.B. einen einzigen Wert definieren.
  • Parameter sind im camelCase zu benennen myParameter.
  • Exceptions sind in PascalCase zu benennen MyException und enden mit Exception.
  • Typen (TypeScript) sind in PascalCase zu benennen MyType.
  • Interfaces (TypeScript) sind in PascalCase zu benennen MyInterface.
  • Symbole sind in UPPER_SNAKE_CASE zu benennen MY_SYMBOL.
  • Enumerations sind in PascalCase zu benennen MyEnum.
  • Objekte sind wie Variablen zu benennen myObject.
javascript
const THE_ANSWER = 42;

function myFunction(myParameter) {
    const myConstant = THE_ANSWER;
    const myObject = { key: "value" };
    const THE_SYMBOL = Symbol("mySymbol");

    if (myObject.key === myConstant) {
        throw new MyException("Error");
    }
}

class MyClass {
    static MY_CLASS_CONSTANT = 42;
    static MyEnum = { RED: Symbol("red") };

    myMethod() {
        const myEnum = MyClass.MyEnum.VALUE;
    }
}
Schein-Konstanten

Objekte oder Array-Inhalte sind immer veränderbar, auch wenn sie mit const deklariert werden. Nur die Zuweisung der Variable ist konstant, nicht der Wert.

Mit Object.freeze() können Objekte und Arrays tatsächlich konstant gemacht werden.

javascript
"use strict"; // TypeError nur im strict mode
const myArray = Object.freeze([1, 2, 3]);

myArray[0] = 4; // TypeError: Cannot assign to read only property '0' of object '[object Array]'

JS7 Reihenfolge der Deklarationen

Die Reihenfolge der Deklarationen soll konsistent sein und die Lesbarkeit des Codes verbessern.

JS7 Reihenfolge in Funktionen und Methoden

Die Deklaration von Variablen und Konstanten innerhalb von Scope-Blöcken soll in folgender Reihenfolge erfolgen:

  1. Umschließender Funktions- oder Block-Scope
    1. Konstanten
    2. Variablen
    3. Funktionen
javascript
function myFunction() {
    const myConstant = 42;
    let myVariable = 42;

    function myInnerFunction() {
        const innerConstant = 42;
        let innerVariable = 42;

        // Code
    }
}

JS7 Reihenfolge in Klassen

In Klassen sollen die Deklarationen in folgender Reihenfolge erfolgen:

  1. Statische Klassen-Konstanten
  2. Statische Klassen-Methoden
  3. Klassen-Konstanten
  4. Klassen-Attribute
  5. Konstruktoren
  6. Klassen-Methoden
  7. Getter und Setter (vermeiden, siehe Tell, don't ask)
  8. Methoden für Symbol.iterator und Symbol.asyncIterator
  9. Methoden für toString, valueOf, toJSON und toPrimitive
  10. Methoden für equals, hashCode, compareTo und compare
  11. Methoden für clone, copy, deepCopy und deepClone
javascript

class MyClass {
    static MY_CLASS_CONSTANT = 42;

    static myStaticMethod() {
        // ...
    }

    MY_CLASS_CONSTANT = 42;
    myAttribute = 42;

    constructor() {
        // ...
    }

    myMethod() {
        // ...
    }

    get myGetter() {
        // ...
    }

    set mySetter(value) {
        // ...
    }

    [Symbol.iterator]() {
        // ...
    }

    toString() {
        // ...
    }

    equals(other) {
        // ...
    }

    clone() {
        // ...
    }
}

JS7 Ausnahmen

  • Zwischenberechnungen für Konstanten oder Variablen können vor der Verwendung deklariert werden, wenn es nicht anders geht.
  • In Fällen, in der eine besser Verständlichkeit des Codes durch eine andere Reihenfolge erreicht wird, kann von der oben genannten Reihenfolge abgewichen werden.

JS8 Verwendung von const und let anstelle von var

var soll nicht verwendet werden, da es zu unerwartetem Verhalten führen kann.

JS8 Problem

var hat eine Funktionsscope und keine Blockscope, was zu unerwartetem Verhalten führen kann.

INFO

Functionscope bedeutet, dass die Variable innerhalb der gesamten Funktion von Anfang an sichtbar ist, auch wenn sie erst später deklariert wird (sogenanntes Hoisting).

Blockscope bedeutet, dass die Variable nur innerhalb des Blocks sichtbar ist, in dem sie deklariert wurde. Blöcke werden mit geschweiften Klammern {} definiert, z.B. in If-Statements, Schleifen oder Funktionen.

JS8 Lösung

const und let haben einen Blockscope und sollen anstelle von var verwendet werden.

Im folgenden Beispiel wird myVariable nur innerhalb des Blocks sichtbar sein:

javascript
const myConstant = 42;
{
    let myVariable = 42;
    //...
}
// myVariable ist hier nicht sichtbar
// myConstant ist hier sichtbar

JS9 Verwendung von const für alle Variablen und Kennzeichnung von Nicht-Konstanten

Variablen enthalten für gewöhnlich Werte, die sich während der Laufzeit des Programms nicht ändern. Eine erneute Zuweisung von Werten zu Variablen kann zu unerwartetem Verhalten führen, weil sich der Wert plötzlich ändert oder versehentlich undefiniert wird.

  • Variablen sollen daher mit const deklariert werden, um sicherzustellen, dass sie nicht versehentlich geändert werden.
  • Eine erneute Zuweisung von Werten zu Variablen soll vermieden werden, um unerwartetes Verhalten zu vermeiden. Stattdessen sollen neue Variablen deklariert werden, wenn ein neuer Wert benötigt wird.
  • Ist eine erneute Zuweisung von Werten notwendig, soll ein Kommentar mit dem Inhalt /*nonconst*/ hinzugefügt werden, um darauf hinzuweisen und dies auch einem Code-Review zu signalisieren, dass der Entwickler sich der Änderung bewusst ist.

JS9 Problem

Die Verwendung von const sorgt dafür, dass Variablen nicht versehentlich geändert werden. Ohne die Verwendung von const besteht die Gefahr, dass Variablen unbeabsichtigt überschrieben werden. Dies kann dazu führen, dass sich der Wert von Variablen, Attributen oder Parametern unerwartet ändert und dadurch unerwünschte Nebeneffekte auftreten können. Dies passiert beispielsweise dann, wenn die Variable, das Attribut oder der Parameter in einem anderen Teil des Codes nachträglich und von einer anderen Person unerwartet geändert wird. Dadurch wird die Lesbarkeit und Nachvollziehbarkeit des Codes erschwert.

javascript
let name = "John";
let age = 30;

// ...

name = "Jane"; // Unbeabsichtigte Änderung der Variable

JS9 Lösung

Um unbeabsichtigtes Ändern von Variablen zu vermeiden, sollen alle Variablen mit const deklariert werden. In Fällen, in denen die Verwendung von const nicht möglich ist (z. B. bei Variablen, die sich ändern müssen), soll ein Kommentar mit dem Inhalt "nonconst" hinzugefügt werden, um darauf hinzuweisen.

javascript
const name = "John";
const age = 30;

// ...

/*nonconst*/ let count = 0;

// oder
/*nonconst*/
let count = 0;

count++;

JS9 Vorteile

  • Vermeidung unbeabsichtigter Änderungen von Variablen
  • Klarheit in Bezug auf die Veränderlichkeit von Variablen
  • Verbesserte Code-Qualität und Verständlichkeit

JS9 Nachteile

Es gibt Situationen, in denen die Verwendung von const nicht möglich oder sinnvoll ist, z. B. bei Variablen, die sich ändern müssen oder in komplexen Legacy-Code. In solchen Fällen kann die Kennzeichnung mit einem Kommentar "//nonconst" helfen, auf die Ausnahme hinzuweisen.

JS10 Einsatz von Linter und Formatter

Tools wie ESLint und Prettier sollen verwendet werden, um sicherzustellen, dass der Code konsistent und weniger fehleranfällig ist.

Mit dem Einsatz von JSDoc können auch Typen geprüft werden.

JS11 Optionaler Operator ?. / Optional Chaining verwenden

Der optionale Operator ?. oder Optional Chaining soll für den Zugriff auf Unterschlüssel verwendet werde, ohne explizit auf null oder undefined prüfen zu müssen.

Alternative

Alternativ kann auch das JS15 Verwendung von Optional in JavaScript-Funktionen. verwendet werden.

JS11 Probleme

In JavaScript besteht oft die Notwendigkeit, auf verschachtelte Schlüssel in Objekten oder Arrays zuzugreifen. Dabei kann es vorkommen, dass einige der Zwischenschlüssel nicht existieren oder dass Methoden undefiniert sein können. In solchen Fällen wird häufig eine Reihe von if-Bedingungen verwendet, um sicherzustellen, dass jeder Schlüssel existiert, bevor auf ihn zugegriffen wird. Dieser Ansatz führt jedoch zu redundantem Code und macht den Code schwerer lesbar und fehleranfällig.

javascript
// Pfad zur Methode könnte nicht existieren
if (myObject && myObject.myKey) {
    myObject.myKey.myMethod();
}

// Falls eine Methode undefiniert sein könnte
if (myObject && myObject.myKey && myObject.myKey.myMethod) {
    myObject.myKey.myMethod();
}

// Prüfung, ob a,b,e und e[0] sowie f nicht null oder undefined sind.
if (a && a.b && a.b.e && a.b.e[0] && a.b.e[0].f != null) {
    // Code ausführen
}

JS11 Lösung

Um den Code übersichtlicher und robuster zu gestalten, kann der optionale Operator ?. (Optional Chaining) verwendet werden. Dieser Operator prüft automatisch, ob der vorherige Schlüssel existiert, und greift nur dann auf den nächsten Schlüssel zu, wenn er vorhanden ist. soll ein Schlüssel nicht existieren, wird keine weitere Aktion ausgeführt und das Ergebnis ist undefined.

javascript
// Pfad zur Methode könnte nicht existieren
myObject.myKey?.myMethod();
// Rückgabe ist `undefined`, falls der Pfad nicht existiert

// Falls eine Methode undefiniert sein könnte
anotherObject.myMethod?.();
// Rückgabe ist `undefined`, falls der Pfad oder die Methode nicht existieren

// Feldzugriff oder Array
anotherObject?.["field"];

if (myNullValue == null) {
    // Code ausführen
}

if (myObject?.myKey?.myMethod()) {
    // Code ausführen
}

// Prüfung, ob a,b,e und e[0] sowie f nicht null oder undefined sind.
a.b.e[0]?.f != null;

// Wenn einer der Schlüssel null oder undefined ist, ist das Ergebnis ebenfalls undefined.
console.log('Defined', a.b.e[0]?.f == null);

JS11 Vorteile

  • Vereinfachung des Codes durch Reduzierung von redundanten if-Bedingungen
  • Lesbarkeit und Wartbarkeit des Codes werden verbessert
  • Verringertes Risiko von Fehlern durch Vergessen oder falsche Anwendung von null- oder undefined-Prüfungen

JS11 Nachteile

  • Keine direkte Unterstützung in älteren JavaScript-Versionen (vor ECMAScript 2020)
  • Verwendung des optionalen Operators kann dazu führen, dass Fehler später erkannt werden, da undefined-Werte nicht sofort als solche erkannt werden
  • Einsatz der Operator nach dem Gießkannenprinzip kann dazu führen, dass andere Entwickler davon ausgehen, dass die Werte nicht immer vorhanden sind und dadurch für Erweiterungen weitere Prüfungen einbauen, die nicht notwendig wären.
Gießkannenprinzip

Das Gießkannenprinzip bezeichnet die Verwendung von optionalen Operatoren an vielen Stellen im Code, auch wenn sie nicht unbedingt notwendig sind.

javascript
const obj = { defined: { value: 42 } };

// Gießkannenprinzip, obj kann nie null oder undefined sein, genauso obj.defined
const value = obj?.defined?.value;

JS11 Weiterführende Informationen

Weitere Informationen zur Verwendung des optionalen Operators ?. oder Optional Chaining in JavaScript findest du in der Mozilla Developer Network (MDN) Dokumentation. Dort werden die Funktionsweise und die verschiedenen Anwendungsfälle ausführlich erläutert.

JS12 Auf null und undefined prüfen

Bei der Prüfung auf null oder undefined soll immer value == null verwendet werden, um sicherzustellen, dass nur null oder undefined erkannt werden.

Anwendungseinsatz und Alternativen

Diese Regel gilt nur für bereits bestehenden Methoden und Funktionen, die null oder undefined zurückgeben können.

Optional soll für neue Methoden/Funktionen verwendet werden, um diese spezielle Fälle (null etc.) zu repräsentieren.

Soll ein neues Klassen- oder Objektmodell erstellt werden, sollen direkt spezielle Objekte verwendet werden.

JS12 Problem

Bei der Überprüfung auf null oder undefined ist es wichtig, die korrekte Überprüfung durchzuführen, da andernfalls unerwartet auch Werte wie 0, "", oder false fälschlicherweise als falsy-Werte erkannt werden können.

javascript
if (!myObject) {
    //myObject === null oder
    //myObject === undefined oder
    //myObject === 0 oder
    //myObject === "" oder
    //myObject === false
}

if (myObject === null || myObject === undefined || typeof myObject === 'undefined') {
    // myObject === null oder
    // myObject === undefined
}

JS12 Lösung

Um sicherzustellen, dass nur null oder undefined erkannt werden und andere falsy-Werte ausgeschlossen werden, kann die folgende Überprüfung verwendet werden:

javascript
if (myObject == null) {
    //myObject === null oder
    //myObject === undefined
}

Die Verwendung von zwei Gleichheitszeichen == anstelle von drei === ist hierbei wichtig, da so undefined und null erkannten werden.

JS12 Vorteile

  • Korrekte Überprüfung auf null oder undefined
  • Vermeidung von unerwarteten Fehlern durch falsche Erkennung von falsy-Werten

JS12 Nachteile

  • Werte wie NaN werden nicht erkannt
  • ESLint muss entsprechend konfiguriert werden, um die Verwendung von == bei null Vergleich zu erlauben. Dies ist möglich, indem die Regel eqeqeq auf smart umgestellt wird.

JS13 Object destructuring / Object Eigenschaften bekommen

Beim Object Destructuring werden die Eigenschaften eines Objekts in einzelne Variablen aufgeteilt und gespeichert.

JS13 Problem

javascript
const car = {
    speed: 10,
    color: "red"
}

const speed = car.speed;
const color = car.color;

JS13 Lösung

Um den Code zu vereinfachen und die Eigenschaften eines Objekts direkt in Variablen zu speichern, kann das Object Destructuring verwendet werden:

javascript
const car = {
    speed: 10,
    color: "red"
}

const { speed, color } = car;

JS13 Vorteile

  • Kürzerer und lesbarer Code
  • Direkter Zugriff auf die gewünschten Eigenschaften des Objekts

JS14 Verwendung von async und await

Die Kombination von async und await soll verwendet werden, um asynchrone Funktionen in JavaScript zu vereinfachen, lesbarer zu machen und insbesondere Verschachtlungen von Callbacks zu vermeiden.

JS14 Problem

Traditionell wurden asynchrone Operationen in JavaScript mithilfe von Callback-Funktionen oder Promises behandelt. Dies führte jedoch oft zu sogenanntem "Callback-Hell" oder zu komplexem und schwer verständlichem Code, insbesondere bei mehreren aufeinanderfolgenden asynchronen Operationen.

javascript
getData(function(result) {
    processData(result, function(data) {
        saveData(data, function(response) {
            // Weitere Operationen...
        });
    });
});

JS14 Lösung

Dank async und await kann asynchroner Code lesbarer und besser handhabbar gemacht werden. Durch das Hinzufügen des async-Schlüsselworts zu einer Funktion wird diese automatisch zu einer asynchronen Funktion. Das await-Schlüsselwort wird verwendet, um auf das Ergebnis einer asynchronen Operation zu warten. Fehlerbehandlung wird durch try/catch durchgeführt.

javascript
async function myAsyncFunction() {
    const result = await getData();
    const data = await processData(result);
    try {
      const response = await saveData(data);
    } catch (e) {
        // ...
    }
    // Weitere Operationen...
}

Wenn auf mehrere asynchrone Operationen gewartet werden muss, kann Promise.all verwendet werden, um die Ausführung zu beschleunigen.

javascript
async function myAsyncFunction() {
    const result = await promise;
    const otherResult = await otherPromise;
    const anotherResult = await anotherPromise;
    // Code ausführen
}
// oder
const result = await Promise.all([promise, otherPromise, anotherPromise])

Exceptions

Es ist wichtig, dass Fehlerbehandlung in asynchronen Operationen nicht vergessen wird.

JS14 Vorteile

  • Lesbarer und verständlicher Code
  • Reduzierung der Verschachtelung von Callbacks ("Callback-Hell")
  • Einfachere Fehlerbehandlung durch Verwendung von try-catch-Blöcken
  • Bessere Kontrolle über asynchrone Abläufe und Reihenfolge der Operationen
  • Einfachere Fehlerbehandlung
  • Fehler werden wie bei Promise nicht verschluckt, sondern müssen explizit behandelt werden.

JS14 Nachteile

  • Verwendung von async und await erfordert ECMAScript 2017 (ES8) oder höher
  • Exception-Handling ist notwendig.

JS15 Begrenzte Zeilenanzahl in Methoden/Funktionen

Codezeilen in Methoden und Funktionen sollen auf eine begrenzte Anzahl beschränkt werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.

Die Begrenzung ist nicht in Stein gemeißelt, aber eine Methode soll nicht mehr als eine Bildschirmseite umfassen bei einer normalen Bildschirmauflösung und Schriftgröße.

Eine Quantisierung von Zeilen ist schwierig, aber eine Möglichkeit ist die Millersche Zahl 7 ± 2 oder die zyklomatische Komplexität.

Millersche Zahl 7 ± 2

Die Millersche Zahl besagt, dass Menschen in der Lage sind, sich an etwa 7 ± 2 Einheiten von Information zu erinnern. Man kann dies auf die Anzahl der Codezeilen übertragen, die ein Entwickler in einer Methode oder Funktion verarbeiten kann.

Siehe auch The Magical Number Seven, Plus or Minus Two

Beachte jedoch, dass diese Zahl umstritten ist und es viele Faktoren gibt, die die Lesbarkeit und das Verständnis von Code beeinflussen.

Zyklomatische Komplexität

Die zyklomatische Komplexität ist eine Softwaremetrik, die die Anzahl der unabhängigen Pfade durch den Quellcode misst. Sie ist gegeben durch die Formel M = E - N + 2P, wobei E die Anzahl der Kanten, N die Anzahl der Knoten und P die Anzahl der Zusammenhangskomponenten ist.

Siehe auch Cyclomatic Complexity oder McCabe-Metrik

Viele Entwicklungsumgebungen bieten eine Möglichkeit, die zyklomatische Komplexität zu berechnen und pro Methode anzuzeigen.

Beachte jedoch auch hier, dass die zyklomatische Komplexität nur ein Indikator für die Komplexität eines Codes ist und nicht alle Aspekte der Lesbarkeit und Wartbarkeit abdeckt.

JS15 Problem

Methoden oder Funktionen mit einer großen Anzahl von Codezeilen können schwer zu lesen, zu verstehen und zu warten sein. Lange Methoden können verschiedene Aufgaben vermischen und die Einhaltung des Single Responsibility Principle erschweren.

javascript
function processUserData(user) {
    // Schritt 1: Validierung der Benutzerdaten
    if (user !== null && user !== undefined) {
        if (validateName(user.name)) {
            if (validateEmail(user.email)) {
                if (validateAge(user.age)) {
                    // ...
                }
            }
        }
    }

    // Schritt 2: Speichern der Benutzerdaten
    if (user !== null && user !== undefined) {
        if (saveUserData(user)) {
            // ...
        }
    }

    // Schritt 3: Senden einer Bestätigungs-E-Mail
    if (user !== null && user !== undefined) {
        if (sendConfirmationEmail(user.email)) {
            // ...
        }
    }

    // Schritt 4: Aktualisierung des Benutzerstatus
    if (user !== null && user !== undefined) {
        if (updateUserStatus(user)) {
            // ...
        }
    }

    // ...
}

JS15 Lösung

Um die Lesbarkeit und Verständlichkeit des Codes zu verbessern, sollen Methoden und Funktionen auf eine begrenzte Anzahl von Zeilen beschränkt sein. Komplexe Aufgaben sollen in kleinere Teilfunktionen ausgelagert werden, um die Verantwortlichkeiten klarer zu trennen.

Die Anzahl von Zeilen soll allgemein so klein wie möglich gehalten werden. Sie soll allerdings nie über eine Bildschirmhöhe hinausgehen, d.h. mehr als 25 Zeilen sollen vermieden werden.

Allgemeine Code-Refactorings sind:

  • Code-Blöcke oder Scopes (durch geschweifte Klammern separiert) können in Methoden ausgelagert werden.
  • Kommentare, die eine Sektion kommentieren können im Allgemeinen in eine Methode ausgelagert werden.
  • For-Schleifen, welche If-Bedingungen beinhalten, können als Methode geschrieben werden.
  • Mehrdimensionale For-Schleifen können in Methoden ausgelagert werden,
  • If-Bedingungen innerhalb einer Methode können als Methode geschrieben werden.
javascript
function processUserData(user) {
    validateUser(user);
    saveUser(user);
    sendConfirmationEmail(user.email);
    updateUserStatus(user);
}

function validateUser(user) {
    if (user === null || user === undefined) {
        throw ...;
    }

    if (!validateName(user.name)) {
        throw ...;
    }

    if (!validateEmail(user.email)) {
        throw ...;
    }

    if (!validateAge(user.age)) {
        throw ...;
    }

    // Weitere Validierungen...
}

function saveUser(user) {
    if (user === null || user === undefined) {
        throw ...;
    }

    if (!saveUserData(user)) {
        throw ...;
    }

    // Weitere Speicheroperationen...
}

// Weitere Teilfunktionen...

JS15 Vorteile

  • Verbesserte Lesbarkeit und Verständlichkeit des Codes durch kleinere und fokussierte Methoden/Funktionen
  • Einfachere Wartbarkeit und Testbarkeit durch klar abgegrenzte Verantwortlichkeiten
  • Bessere Übersichtlichkeit und Strukturierung des Codes
  • Bessere Testbarkeit des Codes, da kleinere Methoden leichter isoliert und getestet werden können
Weitere Gründe für kleine Methoden
  1. KISS-Prinzip: Das KISS-Prinzip kann leichter eingehalten werden, wenn Methoden und Funktionen auf eine begrenzte Anzahl von Zeilen beschränkt sind. Der Entwickler kommt nicht dazu überkomplexe Methoden zu schreiben, da er sich an die Zeilenbeschränkung halten muss.

  2. Bessere Isolierung: Kleine Methoden behandeln normalerweise nur eine spezifische Aufgabe oder Verantwortlichkeit. Dadurch können sie isoliert und unabhängig von anderen Teilen des Codes getestet werden. Durch die Fokussierung auf eine spezifische Funktion können Tests effektiver und einfacher geschrieben werden.

  3. Lesbarkeit: Kleine Methoden sind in der Regel einfacher zu verstehen, da sie nur eine begrenzte Anzahl von Zeilen umfassen und sich auf eine bestimmte Funktionalität konzentrieren. Dadurch wird die Lesbarkeit des Codes verbessert und es ist einfacher, den Zweck und das Verhalten der Methode zu erfassen.

  4. Wiederverwendbarkeit: Kleine Methoden können leichter wiederverwendet werden. Da sie in der Regel spezifische Aufgaben erfüllen, können sie in verschiedenen Teilen des Codes wiederverwendet werden, wenn ähnliche Funktionalität benötigt wird. Dies fördert die Modularität und reduziert die Duplizierung von Code.

  5. Einfache Wartbarkeit: Kleine Methoden sind einfacher zu warten, da sie in sich abgeschlossen sind und Änderungen an einer Methode weniger Auswirkungen auf andere Teile des Codes haben. Bei einem Fehler oder einer gewünschten Änderung ist es einfacher, den relevanten Code zu lokalisieren und anzupassen, ohne den gesamten Kontext berücksichtigen zu müssen.

  6. Bessere Testabdeckung: Durch die Aufteilung des Codes in kleine Methoden wird die Testabdeckung verbessert, da jede Methode spezifische Tests erhalten kann. Dadurch können verschiedene Szenarien und Randbedingungen gezielt getestet werden, um die Fehleranfälligkeit zu reduzieren.

  7. Einfacheres Mocking: Darüber hinaus ist das Mocking in Tests einfacher, wenn der Code in kleine Methoden aufgeteilt ist. Beim Schreiben von Unit-Tests ist es häufig erforderlich, externe Abhängigkeiten zu mocken oder zu fälschen, um den Fokus auf die zu testende Methode zu legen. Mit kleinen Methoden ist es einfacher, diese Abhängigkeiten zu identifizieren und zu isolieren, da sie in der Regel explizit als Parameter an die Methode übergeben werden. Das Mocking-Setup ist zudem kleiner und einfacher, weil aufgespaltete Methoden einfach diese Methoden mocken können, statt die fremde (externe) API, die darin verwendet wird.

  8. Förderung des Single Responsibility Principle: Kleine Methoden unterstützen das Single Responsibility Principle, das besagt, dass eine Methode oder Funktion nur eine einzige Verantwortlichkeit haben soll. Durch die Aufteilung des Codes in kleine Methoden wird die Verantwortlichkeit klarer definiert und das Prinzip der klaren Trennung von Aufgaben eingehalten.

Die Verwendung kleiner Methoden verbessert die Qualität und Wartbarkeit des Codes, indem sie die Testbarkeit, Lesbarkeit, Wiederverwendbarkeit und Modularität fördern. Es ist jedoch wichtig, ein Gleichgewicht zu finden, um eine übermäßige Fragmentierung des Codes zu vermeiden und die Lesbarkeit nicht zu beeinträchtigen.

INFO

Siehe zu Prinzipien und Vorteilen auch Prinzipien der Softwareentwicklung.

JS15 Nachteile

Die strikte Begrenzung der Zeilenanzahl kann zu einer übermäßigen Fragmentierung des Codes führen, wenn kleinere Methoden oder Funktionen unnötig erstellt werden.

JS15 Ausnahmen

Die Anzahl der Codezeilen in einer Methode oder Funktion kann je nach Kontext und Komplexität des Codes variieren.

JS16 Verwenden aussagekräftige Rückgabewerte und -typen

Wenn eine Methode einen Wert zurückgibt, soll dieser Wert aussagekräftig sein und genau das darstellen, was die Methode tut.

JS16 Problem

Oftmals spiegelt der Rückgabe-Wert einer Funktion nicht genau wider, was die Funktion tut. Dies kann zu Verwirrung führen und die Lesbarkeit und Wartbarkeit des Codes beeinträchtigen. Es ist zudem ohne Dokumentation schwer zu verstehen, welche Rückgabewerte eine Funktion haben kann und was diese bedeuten.

Im folgenden Beispiel wird ein Benutzer-Objekt geprüft, jedoch ein String zurückgegeben anstatt eines booleschen Wertes.

javascript
function validate(user) {
    if (user !== null && user !== undefined) {
        return "valid";
    }

    return "invalid";
}

JS16 Lösung

Es können unterschiedliche Ansätze verwendet werden, um aussagekräftige Rückgabewerte und -typen zu verwenden:

  • Verwendung von booleschen Werten (true/false) für Ja/Nein-Entscheidungen
  • Verwendung von spezifischen Werten wie Symbol, um den Status oder das Ergebnis einer Operation darzustellen

Beispiel:

javascript
function findUserById(id) {
  const user = db.findUser(id);
  if (user) {
    return user;
  }
  // statt dessen eine Exception werfen oder ein Optional-Objekt zurückgeben
  throw new UserNotFoundException('User not found');
}

JS17 Methoden/Funktionen geben für alle Pfade einen Wert zurück

Methoden oder Funktionen, die generell einen Wert zurückgeben, müssen für alle Pfade einen Wert zurückgeben.

JS17 Problem

In JavaScript ist es möglich, dass eine Funktion keinen expliziten Rückgabewert hat, was zu unerwartetem Verhalten führen kann. Wenn ein Pfad ein einfaches return-Statement hat, aber andere Pfade nicht, wird undefined implizit zurückgegeben. Ein Benutzer dieser Methode/Funktion, der nicht auf undefined prüft, muss zuerst sicherstellen, dass der Rückgabewert nicht undefined ist, bevor er ihn verwendet. Dieser Vorgang wird oft vergessen und kann zu Fehlern führen, die schwer zu finden sind.

Generell ist der Rückgabewert null keine Lösung, da er sich von undefined nur in der Semantik unterscheidet.

javascript
function getUserById(id) {
    if (id === 1) {
        return { id: 1, name: 'Alice' };
    }
    // Kein explizites return-Statement.
    // Es wird implizit `undefined` zurückgegeben.
}

JS17 Lösung

  • Eine Funktion soll nie undefined oder null zurückgeben, sondern immer einen Wert oder ein Objekt, das den Status des Ergebnisses darstellt.
  • Jeder Ablaufpfad in einer Funktion muss ein return-Statement haben, um sicherzustellen, dass immer ein Wert zurückgegeben wird.
  • Statt undefined kann auch Optional verwendet werden, um den Status des Ergebnisses zu kennzeichnen. Siehe dazu auch JS15 Verwendung von Optional in JavaScript-Funktionen.
  • Es kann auch ein spezieller Wert als Objekt zurückgegeben werden, um den Status des Ergebnisses zu kennzeichnen (siehe Beispiel unten).
javascript
class EmptyUser extends User {
    static create() {
       return Object.freeze(new EmptyUser())
    }
    constructor() {
        super(-1, 'Unknown');
    }
    isValid() {
        return false;
    }
}

class User {
    static EMPTY = EmptyUser.create();

    isValid() {
        return true;
    }
    // ...
}

function getUserById(id) {
    if (id === 1) {
        return new User(1, 'Alice'); //oder Erstellung über Factory
    }
    return User.EMPTY; // oder return Optional.empty();
}

Hinweis

Funktionen/Methoden, die generell keinen Wert zurückgeben, fallen nicht unter diese Regel.

JS17 Ausnahmen

Generell kann auch null ein gültiger Rückgabewert sein, wenn er explizit verwendet wird, um einen speziellen Zustand oder eine Bedeutung zu kennzeichnen. Dieser Zustand muss jedoch in der JSDoc-Dokumentation klar dokumentiert sein, um Missverständnisse zu vermeiden. Generell sollte man solche Methoden jedoch nur für interne Zwecke verwenden und nicht als öffentliche API bereitstellen.

JS18 Methoden/Funktionen, die Mengen zurückgeben sollen niemals null oder undefined zurückgeben

Methoden oder Funktionen, die Mengen wie Arrays zurückgeben, sollen nie null oder undefined zurückgeben, sondern leere Mengen oder Objekte.

JS18 Problem

Das Zurückgeben von null als Ergebnis einer Methode/Funktion, die eine Liste, HashMap oder ein Array zurückgibt, kann zu Zugriffsfehlern (undefined) und unerwartetem Verhalten führen. Es erfordert zusätzliche Überprüfungen auf null und erhöht die Komplexität des Aufrufers.

javascript
getNames() {
    if (condition) {
        return null;
    }
    // ...
}

JS18 Lösung

Um Zugriffsfehler und unerwartetes Verhalten zu vermeiden, sollen Methoden/Funktionen stattdessen ein leeres Objekt oder Array zurückgeben.

javascript
function getNames() {
    if (condition) {
        return [];
    }
    // ... return xy
}

JS18 Vorteile

  • Vermeidung von Zugriffsfehlern und unerwartetem Verhalten
  • Einfachere Handhabung und weniger Überprüfungen auf null im Aufrufercode
  • Verbesserte Robustheit und Stabilität des Codes

JS18 Ausnahmen

Es kann Situationen geben, in denen die Rückgabe von null sinnvoll ist, z. B. wenn null einen speziellen Zustand oder eine Bedeutung hat. In solchen Fällen ist es wichtig, die Dokumentation klar zu kommunizieren und sicherzustellen, dass der Aufrufer angemessen darauf reagiert.

JS19 Verwendung von Optional in JavaScript-Funktionen

Eine Funktion oder Methode, die dennoch null, undefined oder NaN zurückgeben muss, soll stattdessen die Optional-Klasse verwenden, um den Status des Ergebnisses zu kennzeichnen.

Anwendungseinsatz und Alternativen

Diese Regel gilt nur für bereits bestehenden Klassenstrukturen, die nicht veränderbar oder erweiterbar sind.

Soll ein neues Klassen- oder Objektmodell erstellt werden, sollen direkt spezielle Objekte verwendet werden.

JS19 Problem

Das Zurückgeben von null, undefined oder NaN aus einer Funktion kann zu Fehlern führen, insbesondere wenn nicht überprüft wird, ob das Ergebnis vorhanden ist oder nicht.

javascript
function divide(a, b) {
  if (b !== 0) {
    return a / b;
  }
  return null;
}

JS19 Lösung

Die Verwendung des Optional-Objekts ermöglicht es, den Status des Ergebnisses klar zu kennzeichnen, anstatt null, undefined oder NaN zurückzugeben.

javascript
function divide(a, b) {
  if (b !== 0) {
    return Optional.of(a / b);
  }

  return Optional.empty();
}

JS19 Vorteile

  • Klarere Kennzeichnung des Zustands des Ergebnisses durch Verwendung von Optional
  • Bessere Fehlervermeidung durch explizite Behandlung von leeren Ergebnissen
  • Verbesserte Lesbarkeit des Codes durch den Verzicht auf null oder undefined

JS19 Nachteile

  • Zusätzlicher Overhead durch die Verwendung von Optional
  • Potenziell erhöhter Komplexitätsgrad in der Verwendung des Optional-Objekts
  • Abhängigkeit von der Implementierung der Optional-Klasse

JS19 Ausnahmen

Die Verwendung von Optional in JavaScript ist eine Designentscheidung und keine Sprachfunktion. Es ist optional und soll basierend auf den Anforderungen des Projekts und der Teampräferenz eingesetzt werden.

Codebeispiel für die Optional-Klasse in JavaScript
javascript

/**
 * Klasse, die ein optionales Ergebnis repräsentiert.
 * @param {*} value - Der Wert des optionalen Ergebnisses.
 * @class
 * @readonly
 */
function Optional(value) {
  /**
   * Der Wert des optionalen Ergebnisses.
   * @type {*}
   * @readonly
   */
  Object.defineProperty(this, 'result', {
    value: value,
    writable: false
  });

  /**
   * Gibt an, ob das optionale Ergebnis vorhanden ist.
   * @type {boolean}
   * @readonly
   */
  Object.defineProperty(this, 'isPresent', {
    get: function() {
      return this.result !== null && !isNaN(this.result);
    }
  });

  /**
   * Gibt an, ob das optionale Ergebnis fehlt.
   * @type {boolean}
   * @readonly
   */
  Object.defineProperty(this, 'isMissing', {
    get: function() {
      return !this.isPresent;
    }
  });

  Object.freeze(this);
}

/**
 * Erstellt ein `Optional`-Objekt mit dem angegebenen Wert.
 * @param {*} value - Der Wert für das optionale Ergebnis.
 * @returns {Optional} - Das erstellte `Optional`-Objekt.
 * @static
 * @example
 * var myResult = Optional.of(42);
 */
Optional.of = function(value) {
  return new Optional(value);
};

/**
 * Erstellt ein leeres `Optional`-Objekt.
 * @returns {Optional} - Das erstellte leere `Optional`-Objekt.
 * @static
 * @example
 * var emptyResult = Optional.empty();
 */
Optional.empty = function() {
  return new Optional(null);
};

/**
 * Überprüft, ob ein gegebener Wert eine Instanz von `Optional` ist.
 * @param {*} optional - Der zu überprüfende Wert.
 * @returns {boolean} - `true`, wenn der Wert eine `Optional`-Instanz ist, andernfalls `false`.
 * @static
 * @example
 * var myResult = Optional.of(42);
 * console.log(Optional.isOptional(myResult)); // true
 */
Optional.isOptional = function(optional) {
  return optional instanceof Optional;
};

JS20 Verwendung der npm-Bibliothek optional.js zur Rückgabe von Optional in JavaScript

Es ist einfach in JavaScript, die npm-Bibliothek optional.js zu verwenden, um die Rückgabe von Optional-Objekten anstelle von null oder anderen Fehlertypen zu ermöglichen. Durch die Verwendung von Optional-Objekten wird deutlich, dass eine Funktion möglicherweise keinen Wert zurückgibt und ermöglicht eine bessere Behandlung von optionalen Werten.

Abhängigkeiten minimieren

Es kann besser sein, die Abhängigkeiten des eigenen Projekts zu minimieren und so eine Optional-Klasse selbst zu implementieren, um die Abhängigkeit von optional.js zu vermeiden.

JS20 Problem

In JavaScript ist es üblich, dass Funktionen null, undefined oder andere Werte zurückgeben, um das Fehlen eines erwarteten Werts zu kennzeichnen. Dies kann zu Verwirrung und unerwartetem Verhalten führen, da der Rückgabewert möglicherweise nicht explizit auf das Fehlen des Werts überprüft wird.

javascript
function findUserById(id) {
  const user = db.findUser(id);
  if (user) {
    return user;
  }
  return null;
}

const user = findUserById(123);
if (user) {
  // Do something with the user
}

JS20 Lösung

Durch die Verwendung der optional.js-Bibliothek können Funktionen Optional-Objekte zurückgeben, um das Fehlen eines Werts explizit zu kennzeichnen. Dadurch wird die Code-Klarheit verbessert und die Behandlung von optionalen Werten erleichtert.

javascript
const Optional = require('optional-js');

function findUserById(id) {
  const user = db.findUser(id);
  return Optional.ofNullable(user);
}

const userOptional = findUserById(123);
if (userOptional.isPresent()) {
  const user = userOptional.get();
  // Do something with the user
}

JS20 Vorteile

  • Bessere Behandlung von optionalen Werten durch die Verwendung von Optional-Objekten
  • Explizite Kennzeichnung des Fehlens eines Werts
  • Einfachere Überprüfung des Vorhandenseins eines Werts und Zugriff auf den Wert mit den Methoden von Optional

JS20 Nachteile

  • Einführung einer zusätzlichen Abhängigkeit durch die Verwendung der optional.js-Bibliothek
  • API Methoden, die optional.js-Bibliothek verwenden erfordern, dass Nutzer der API die optional.js-Bibliothek einbinden, kennen und verwenden müssen.

JS20 Ausnahmen

Es kann Situationen geben, in denen die Verwendung der optional.js-Bibliothek nicht angemessen ist, z. B. wenn das Projekt bereits eine andere Lösung für die Behandlung von optionalen Werten verwendet oder wenn die Einführung einer zusätzlichen Abhängigkeit vermieden werden soll.

JS21 If-Bedingungen ohne Else und mit Return

If-Bedingungen, die ein Return enthalten, sollen kein else enthalten, um die Lesbarkeit des Codes zu verbessern und die Verschachtelung von Bedingungen zu reduzieren.

Im Folgenden sind sich widersprechende Regeln aufgeführt, die bei der Reihenfolge der Bedingungen in If-Statements zu beachten sind:

  • Die Bedingung, welche am wenigsten Code enthält, sollte zuerst geprüft werden.
  • Die Bedingung, welche die Funktion/Methode am schnellsten verlässt, sollte zuerst geprüft werden.
  • Die Bedingung, welche eine Exception wirft, sollte zuerst geprüft werden.
  • Die Bedingung, welche eine positive Bedingung prüft, sollte zuerst geprüft werden.
  • Die Bedingung, welche am häufigsten zutrifft, sollte zuerst geprüft werden.

JS21 Problem

If-Bedingungen mit einem Return und einem dazugehörigen else-Block können die Lesbarkeit des Codes beeinträchtigen und zu unnötiger Verschachtelung führen.

javascript
function calculate(x) {
  if (x > 0) {
    return x * 2;
  } else if (x < 0) {
    return x;
  } else {
    return 42;
  }
}

JS21 Lösung

Durch Entfernen des else-Blocks und direktes Rückgabestatement wird der Code lesbarer und die Verschachtelung von Bedingungen reduziert.

javascript
function calculate(x) {
  if (x > 0) {
    // Guard Clause
    return x * 2;
  }
  // Guard Clause #2
  if (x < 0) {
    return x;
  }
  return 42;
}

JS21 Vorteile

  • Verbesserte Lesbarkeit und Klarheit des Codes
  • Pfade durch die Funktion sind klarer und leichter nachvollziehbar
  • Reduzierung der Verschachtelung von Bedingungen
  • Vereinfachte Struktur und Fluss des Codes

JS22 Guard Pattern

Guard-Klauseln sollen verwendet werden, um unerwünschte Ausführungszweige frühzeitig zu beenden und die Lesbarkeit des Codes zu verbessern.

Im Folgenden sind sich widersprechende Regeln aufgeführt, die bei der Reihenfolge der Bedingungen in If-Statements zu beachten sind:

  • Die Bedingung, welche am wenigsten Code enthält, sollte zuerst geprüft werden.
  • Die Bedingung, welche die Funktion/Methode am schnellsten verlässt, sollte zuerst geprüft werden.
  • Die Bedingung, welche eine Exception wirft, sollte zuerst geprüft werden.
  • Die Bedingung, welche eine positive Bedingung prüft, sollte zuerst geprüft werden.
  • Die Bedingung, welche am häufigsten zutrifft, sollte zuerst geprüft werden.

JS22 Problem

In JavaScript müssen oft komplexe Bedingungen geprüft werden, um unerwünschte Ausführungszweige zu verhindern oder ungültige Eingaben abzufangen. Dies kann zu verschachteltem Code führen, der schwer zu lesen und zu warten ist.

javascript
function processInput(input) {
    if (input && input > 0) {
        // Code zur Verarbeitung des Eingabewerts
    }
}

JS22 Lösung

Das Guard Pattern ermöglicht es, Bedingungsprüfungen klarer und lesbarer zu gestalten, indem wir unerwünschte Fälle frühzeitig abfangen und beenden.

javascript
function processInput(input) {
    if (!input || input <= 0) {
        return;
    }

    // Code zur Verarbeitung des Eingabewerts
}

JS22 Vorteile

  • Verbesserte Lesbarkeit des Codes durch eine klare und frühzeitige Abhandlung unerwünschter Fälle
  • Reduzierung der Verschachtelung von Bedingungsprüfungen
  • Einfache Erweiterbarkeit und Wartbarkeit des Codes

JS23 Positiv formulierte If-Bedingungen und Auslagerung komplexer Bedingungen

If-Bedingungen sollen positiv formuliert werden und komplexe Bedingungen sollen in temporäre Variablen ausgelagert werden, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.

Beachte

Die Aufsplittung von If-Bedingungen ist sehr abhängig vom Verständnis des Entwicklers und soll mit Sinn und Verstand eingesetzt werden.

Generell ist die KISS-Regel (Keep It Simple, Stupid) zu beachten.

JS23 Problem

Komplexe Bedingungen in If-Anweisungen können den Code schwer verständlich machen, insbesondere wenn sie negativ formuliert sind. Lange und verschachtelte Bedingungen erschweren die Lesbarkeit und können zu Fehlern führen.

java
if (!(name.isEmpty() || age < 18 || !isAuthorized)) {
    // Code ausführen
}
javascript
if (!(name === "" || age < 18 || !isAuthorized)) {
    // Code ausführen
}

JS23 Lösung

Durch die positive Formulierung der Bedingungen und die Auslagerung komplexer Ausdrücke in temporäre Variablen wird der Code lesbarer und verständlicher. Nicht jede Bedingung muss dabei in eine temporäre Variable ausgelagert werden, sondern nur diejenigen, die die Lesbarkeit des Codes verbessern.

java
boolean hasName = !name.isEmpty();
boolean isOfAge = age >= 18;

if (hasName || isOfAge || !isAuthorized) {
    // Code ausführen
}
javascript
const hasName = name !== "";
const isOfAge = age >= 18;

if (hasName || isOfAge || !isAuthorized) {
    // Code ausführen
}

JS23 Vorteile

  • Verbesserte Lesbarkeit des Codes durch positiv formulierte Bedingungen
  • Reduzierung der Verschachtelung und Komplexität von If-Anweisungen
  • Bessere Wartbarkeit des Codes durch klare und beschreibende Variablen

JS23 Nachteile

  • Alternativ kann ein Kommentar die If-Bedingung beschreiben, aber bei einer Änderung muss daran gedacht werden auch den Kommentar anzupassen.
  • Das Auslagern von Bedingungen in temporäre Variablen kann zu einem erhöhten Speicherverbrauch führen, insbesondere bei komplexen Ausdrücken. Dies ist normalerweise vernachlässigbar, kann jedoch in speziellen Situationen berücksichtigt werden.

JS23 Ausnahmen

Es gibt Fälle, in denen das Auslagern von Bedingungen in temporäre Variablen nicht sinnvoll ist, z. B. wenn die Bedingung nur an einer Stelle verwendet wird und keine weitere Klarheit oder Wartbarkeit gewonnen wird.

JS24 Exceptions in JavaScript nicht einfach loggen und unverändert wieder werfen

Exceptions sollen in JavaScript nicht einfach nur geloggt und unverändert wieder geworfen werden.

Stattdessen ist es wichtig, Exceptions sinnvoll zu behandeln und angemessene Maßnahmen zu ergreifen.

Wichtig

Entweder wird die Exception geloggt und behandelt ODER in eine andere Form umgewandelt und geworfen.

Aber nicht beides.

JS24 Problem

Das einfache Loggen und unveränderte Werfen von Exceptions führt oft dazu, dass die eigentliche Ursache des Problems verschleiert wird. Es erschwert auch die Fehlerbehandlung und das Debugging des Codes.

javascript
try {
  // Code, der eine Exception auslöst
} catch (error) {
  console.log('Exception aufgetreten:', error);
  throw error;
}

JS24 Lösung

Es ist wichtig, die Ursache der Exception zu verstehen und entsprechend zu reagieren. Dies kann das Ergreifen von Fehlerbehandlungsmaßnahmen, das Aufzeigen von aussagekräftigen Fehlermeldungen oder das Umwandeln der Exception in eine andere Form sein.

javascript
try {
  // Code, der eine Exception auslöst
} catch (error) {
  // Fehlerbehandlung und angemessene Maßnahmen ergreifen
  console.error('Ein Fehler ist aufgetreten:', error);
  // Weitere Maßnahmen wie Fehlermeldung anzeigen, alternative Verarbeitung, etc.
}

oder die Exception kann durch eine sprechendere Exception ersetzt werden:

java
try {
  // Code, der eine Exception auslöst
} catch (error) {
  throw new CustomException('Fehler beim Verarbeiten der Daten', error);
}

JS24 Vorteile

  • Klare Behandlung und Reaktion auf Exceptions
  • Verbesserte Fehlerbehandlung und Debugging-Möglichkeiten
  • Besseres Verständnis der Ursachen von Fehlern

JS24 Ausnahmen

In einigen Fällen kann es sinnvoll sein, Exceptions zu loggen und unverändert wieder zu werfen. Dies ist jedoch eher die Ausnahme und soll gut begründet sein, z.B. wenn der Code in einem bestimmten Kontext läuft, der spezielle Anforderungen hat.

JS25 Benennung von Methoden mit verschiedenen Präfixen für Synchronität und Ergebnisverhalten

Es ist eine bewährte Praxis bei der Benennung von Methoden in JavaScript und Java, unterschiedliche Präfixe zu verwenden, um die Synchronität und das Ergebnisverhalten der Methode zu kennzeichnen. Das Präfix "get" soll für synchronen Zugriff verwendet werden und immer einen Wert zurückgeben, während die Präfixe "fetch" oder "request" für asynchronen Zugriff stehen, der länger dauern und auch fehlschlagen kann.

get-Präfix

Verwechsle das get-Präfix nicht mit dem get-Präfix in Java, das für Getter-Methoden verwendet wird. get- in JavaScript soll für synchronen Zugriff für eine berechnete Eigenschaft oder ein Ergebnis stehen.

JS25 Problem

Bei der Benennung von Methoden ist es wichtig, klare Hinweise auf die Synchronität und das Ergebnisverhalten der Methode zu geben. Unklare oder inkonsistente Benennung kann zu Missverständnissen und unerwartetem Verhalten führen.

javascript
// Unklare Benennung ohne klare Angabe zur Synchronität und zum Ergebnisverhalten
function getData() {
  // ...
}

// Unklare Benennung ohne klare Angabe zur Synchronität und zum Ergebnisverhalten
async function getAsyncData() {
  // ...
}

JS25 Lösung

Um die Synchronität und das Ergebnisverhalten einer Methode klarer zu kennzeichnen, sollen unterschiedliche Präfixe verwendet werden. Das Präfix "get" soll für synchronen Zugriff verwendet werden und immer einen Wert zurückgeben. Die Präfixe "fetch" oder "request" sollen für asynchronen Zugriff stehen, der länger dauern und auch fehlschlagen kann.

get-Präfixe sollen nie async sein, dagegen sollen fetch- oder request- Präfixe immer async sein.

javascript
// Synchroner Zugriff mit Wert-Rückgabe
function getValue() {
  // ...
}

// Asynchroner Zugriff mit Möglichkeit eines Fehlschlags
async function fetchResource() {
  // ...
}

JS25 Vorteile

  • Klare und eindeutige Benennung, die die Synchronität und das Ergebnisverhalten einer Methode widerspiegelt
  • Verbesserte Lesbarkeit und Verständlichkeit des Codes
  • Einfachere Fehlersuche und Debugging-Möglichkeiten

JS25 Ausnahmen

Es kann Situationen geben, in denen die Verwendung von anderen Präfixen angemessen ist, abhängig von den spezifischen Anforderungen und Konventionen des Projekts. Es ist wichtig, einheitliche Namen innerhalb des Projekts festzulegen und zu dokumentieren.

JS26 JSDoc Kommentare für Methoden, Funktionen, Variablen, Objekte und Typen

Methoden, Funktionen, Variablen, Objekte und Typen in JavaScript sollen mit JSDoc annotiert werden, um eine klare Dokumentation und Typisierung der Parameter und des Rückgabewerts zu ermöglichen.

JS26 Problem

JavaScript ist eine dynamisch typisierte Sprache, was zu einer geringeren Typsicherheit und Dokumentation führen kann. Parameter, Variablen und Rückgabewerte von Methoden und Funktionen sind nicht explizit typisiert und dokumentiert, was zu Verwirrung und Fehlern führen kann.

JS26 Lösung

Die Verwendung von JSDoc ermöglicht es, Methoden, Funktionen, Variablen, Objekte und Typen in JavaScript klar zu dokumentieren und zu typisieren. Auf diese Art können auch Objekte und jede andere Art von Datenstrukturen dokumentiert werden.

INFO

Moderne Entwicklungsumgebungen und Tools wie Visual Studio Code, WebStorm und ESLint unterstützen JSDoc-Kommentare und bieten Funktionen wie Autovervollständigung, Typüberprüfung und Dokumentation. Diese Tools melden Probleme bei inkompatiblen Typen und fehlenden Parametern oder Rückgabewerten.

JS26 Beispiele

JS26 Methoden und Funktionen

Beachte!

JSDoc-Kommentare beginnen mit /** und enden mit */. Jede Zeile innerhalb des Kommentars beginnt mit *.

javascript
/**
 * Berechnet die Summe von zwei Zahlen.
 * @param {number} x Die erste Zahl.
 * @param [number] y Die zweite Zahl ist optional. //Alternative Google Closure Compiler Syntax: {number=}
 * @param [string|number] text Ein Text als String oder Zahl.
 * @param {*} data Ein beliebiger Typ.
 * @param {number} [offsetDefault=1] - Der Standardwert, falls der Parameter fehlt.
 * @return {number} Die Summe der beiden Zahlen.
 * @throws {Error} Wenn die Eingabe ungültig ist.
 */

JS26 Variablen

javascript
/**
 * Die maximale Anzahl von Elementen.
 * @type {number}
 */
const MAX_ELEMENTS = 100;

/** @type {(symbol|boolean|{}|string|Array.<string>|number|null|NaN)} */
let myVariable;
myVariable = Symbol('mySymbol');
myVariable = true;
myVariable = {};
myVariable = 'Hello';
// oder, entspricht string[]
myVariable = ['Hello', 'World'];
myVariable = 42;
myVariable = null;
myVariable = NaN;


/** @type {?number} */
let nullableVar = null;

/** @type {!number} */
let NotNullVar = 0;

JS26 Objekte deklarieren

Objekt-Variablen können direkt mit @type dokumentiert werden.

javascript
 /**
 * Ein Benutzerobjekt.
 * @type {Object}
 * @property {string} name Der Name des Benutzers.
 * @property {number} age Das Alter des Benutzers.
 * @property {{street: string, city: string}} address Die Adresse des Benutzers.
 * @property {string[]} roles Die Rollen des Benutzers.
 */
let user = {
  name: 'Alice',
  age: 30,
  address: {
    street: '123 Main St',
    city: 'Anytown'
  },
  roles: ['admin', 'user']
};

JS26 Typen definieren

Wenn ein Objekt mehrmals verwendet wird, kann der Typ mit @typedef definiert werden.

@typedef definiert einen benutzerdefinierten Typ, der in anderen JSDoc verwendet werden kann. Im Gegensatz zu @type, welches den Typ einer Variablen oder eines Objekts angibt, definiert @typedef einen benutzerdefinierten Typ, der wiederverwendet werden kann.

javascript

 /**
 * Ein Benutzerobjekt.
 * @typedef {Object} User
 * @type {Object}
 * @property {string} name Der Name des Benutzers.
 * @property {number} age Das Alter des Benutzers.
 */

// definiert den Typ der Variable user als User-Objekt
/**
 * @type {User}
 */
let user;

JS26 Siehe dazu

JS27 Variable Parameter in Funktionen oder Methoden vermeiden

Variable Parameter in Funktionen oder Methoden sollen vermieden werden, wenn bereits Parameter mit spezifischen Typen oder Strukturen definiert sind.

JS27 Problem

Variable Parameter in Funktionen oder Methoden in Kombination mit weiteren vorangestellten unterschiedlichen Parametern können zu Verwirrung und unerwartetem Verhalten führen.

javascript
function fetchData(url, headers, options, ...params) {
  // ...
}

JS27 Lösung

Verwende stattdessen spezifische Parameter oder separate Funktionen/Methoden, um das Verhalten klarer zu kennzeichnen.

javascript
function fetchData(url, headers, options) {
  // ...
}

function fetchDataWithParams(url, ...params) {
  // ...
}

JS27 Ausnahmen

Wenn die Funktion oder Methode nur ein vorangestellten Parameter besitzt, kann der Restparameter ...params verwendet werden, um eine variable Anzahl von Argumenten zu akzeptieren. Eine Verwechslung mit den vorangestellten Parametern ist in diesem Fall unwahrscheinlich.

javascript
function formatString(template, ...params) {
  // ...
}

Variable Parameter kombiniert mit spezifischen Parametern

Betrachte folgendes Beispiel:

javascript
function useOneOrMultiple(first, ...rest) {
  console.log(first);
  console.log(rest); // rest ist ein Array, d.h ...rest sind die Inhalte
}

const args = [1, 2, 3];
useOneOrMultiple(args[0], ...args.slice(1));
useOneOrMultiple(args[0], args.slice(1));
useOneOrMultiple(...args);
useOneOrMultiple(args);

Die Ausgabe ist:

  • 1 und [2, 3]
  • 1 und [[2, 3]]
  • 1 und [2, 3]
  • [1, 2, 3] und []

Erklärung:

  • useOneOrMultiple(args[0], ...args.slice(1)); entspricht useOneOrMultiple(1, 2, 3);, da ...args.slice(1) in (2, 3) aufgelöst wird.
  • useOneOrMultiple(args[0], args.slice(1)); entspricht useOneOrMultiple(1, [2, 3]);
  • useOneOrMultiple(...args); entspricht useOneOrMultiple(1, 2, 3); , da (...args) in (1, 2, 3) über alle Parameter aufgelöst wird wie beim ersten Beispiel
  • useOneOrMultiple(args); entspricht useOneOrMultiple([1, 2, 3], []);

Jetzt stell dir vor, dass es mehr als 2 spezifische Parameter gibt und du versuchst, die Argumente zu übergeben...

JS28 Boolean-Parameter in Funktionen oder Methoden vermeiden

Boolean als Parameter in Funktionen oder Methoden sollen nicht verwendet werden. Stattdessen sollen eigene Funktionen oder Methoden mit entsprechenden Namen und Parametern erstellt werden, weil damit das Verhalten der Funktion oder Methode klarer wird.

JS28 Problem

Boolean-Parameter in Funktionen oder Methoden können zu Verwirrung und unerwartetem Verhalten führen, da der Aufrufer den Zweck des Parameters erraten muss.

javascript
function fetchData(url, async) {
  if (async) {
    // Asynchroner Aufruf
  } else {
    // Synchroner Aufruf
  }
}

JS28 Lösung

Verwende stattdessen spezifische Parameter oder separate Funktionen/Methoden, um das Verhalten klarer zu kennzeichnen.

javascript
function fetchAsyncData(url) {
  // Asynchroner Aufruf
}

function fetchData(url) {
  // Synchroner Aufruf
}

JS29 Default Parameter in Funktionen oder Methoden

Default Parameter in Funktionen oder Methoden sollen nicht verwendet werden.

JS29 Problem

Default Parameter in Funktionen oder Methoden können zu unerwartetem Verhalten führen, wenn der Aufrufer den Standardwert nicht erwartet oder überschreibt.

Sollte der default Parameter sich später ändern, kann dies zu unerwartetem Verhalten führen bei Code, der bereits existiert und den Standardwert verwendet.

javascript
function increment(value, step = 10) {
  return value + step;
}

JS29 Lösung

Verwende stattdessen separate Funktionen oder Methoden mit spezifischen Namen, um das Verhalten klarer zu kennzeichnen.

javascript
function incrementValueBy(value, step) {
  return value + step;
}

function incrementByTen(value) {
  return increment(value, 10);
}

JS29 Vorteile

  • Klarere und verständlichere Funktionen und Methoden
  • Vermeidung von unerwartetem Verhalten durch Standardwerte
  • Einfachere Wartung und Erweiterung des Codes
  • Nachträgliches Refactoring bzw. Änderungen des Standardwertes sind einfach, weil einfach eine neue Funktion erstellt wird.

JS29 Nachteile

  • Mehr Code und mehr Tests, da separate Funktionen oder Methoden erstellt werden müssen
  • Möglicherweise mehr Code-Duplizierung, wenn die Funktionen oder Methoden ähnliche Logik enthalten
  • Mehr Aufwand bei der Benennung von Funktionen oder Methoden
  • Mehr Aufwand bei der Dokumentation von Funktionen oder Methoden

JS30 Template Strings

Template Literals (`) sollen verwendet werden, um Strings in JavaScript zu erstellen, anstatt die veraltete Methode mit + zu verwenden.

JS30 Beispiel

javascript
const name = 'Alice';

// Veraltete Methode
console.log('Hello, ' + name + '!');
// Moderne Methode
console.log(`Hello, ${name}!`);

JS31 Spread-Operator

Der Spread-Operator (...) soll verwendet werden, um Arrays zusammenzuführen oder Objekte zu kopieren, anstatt Schleifen oder Methoden wie concat() oder Object.assign() zu verwenden.

JS31 Problem

Um Arrays zusammenzuführen oder Objekte zu kopieren, werden oft Schleifen oder Methoden wie concat() oder Object.assign() verwendet. Dies ist jedoch umständlich und unübersichtlich.

JS31 Lösung

Der Spread-Operator (...) ermöglicht es, Arrays zusammenzuführen oder Objekte zu kopieren, indem er die Elemente oder Eigenschaften in ein neues Array oder Objekt einfügt.

JS31 Arrays zusammenführen

javascript
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

const result = [...arr1, ...arr2];

Objekte im Array

Der Spread-Operator erstellt eine flache Kopie der Elemente, d.h. wenn die Elemente Objekte sind, werden nur die Referenzen kopiert.

Dies gilt auch für alle anderen Arten von Objekten, die referenziert werden, wie z.B. Funktionen, Maps, Sets, etc.

javascript
const arr1 = [{ a: 1 }, { b: 2 }];
const arr2 = [{ c: 3 }, { d: 4 }];

const result = [...arr1, ...arr2];

arr1[0].a = 42;
console.log(result[0]); // { a: 42 }

JS31 Objekte kopieren

javascript
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

const copy = { ...obj1, ...obj2 };

Objekte im Objekt

Der Spread-Operator erstellt eine flache Kopie der Eigenschaften, d.h. wenn die Eigenschaften Objekte sind, werden nur die Referenzen kopiert.

javascript
const obj1 = { a: { x: 1 }, b: { y: 2 } };
const obj2 = { c: { z: 3 }, d: { w: 4 } };

const copy = { ...obj1, ...obj2 };

obj1.a.x = 42;
console.log(copy.a.x); // 42

Wenn Objekte die gleichen Keys haben, wird der letzte Wert verwendet.

javascript
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };

const copy = { ...obj1, ...obj2 };

console.log(copy); // { a: 1, b: 3, c: 4 }
Tiefe Kopie

Um eine tiefe Kopie von Objekten zu erstellen, die auch die Eigenschaften der Objekte kopiert, kann das globale Objekt JSON verwendet werden.

javascript
const obj1 = { a: { x: 1 }, b: { y: 2 } };

const deepCopy = JSON.parse(JSON.stringify(obj1));

Beachte jedoch:

  • Operation sind nicht für alle Objekte geeignet ist,
    • z.B. für Funktionen, Maps, Sets,
    • oder zirkuläre Referenzen.
  • Bei einer tiefen Kopie gehen auch Methoden und Prototypen verloren.
  • Die Performance ist schlechter als bei flachen Kopien.

JS32 Arrow Funktion statt function

Arrow Functions (() => {}) sollen verwendet werden, um Funktionen in JavaScript zu definieren, anstatt der traditionellen function-Syntax.

me = this soll vermieden werden.

JS32 Problem

Traditionelle Funktionen (function) haben einen eigenen Wert für this, was es umständlich macht, auf das äußere this zuzugreifen.

javascript
function Person() {
  this.age = 0;

  setInterval(function growUp() {
    // `this` bezieht sich auf den globalen Objekt, nicht auf die Person-Instanz
    this.age++;
  }, 1000);
}
bind-Methode von Funktionen

Eine Möglichkeit, das Problem zu lösen, ist die Verwendung von bind(). Bind erstellt eine neue Funktion, die, wenn sie aufgerufen wird, ihren this-Wert auf den angegebenen Wert setzt.

javascript
function Person() {
  this.age = 0;

  setInterval(function growUp() {
    this.age++;
  }.bind(this), 1000);
}

Beachte, das ein einmal gebundener Wert nicht mehr geändert werden kann.

javascript
function callCallback(callback) {
  callback.call(null); // `this` wird auf `null` gesetzt, aber durch `bind` nicht mehr änderbar
}

callCallback(function() {
  console.log(this);  // nicht null, sondern das ursprüngliche `this`
}.bind(this)); // this ist jetzt gebunden

JS32 Lösung

Arrow Functions (() => {}) haben kein eigenes this, sondern erben es vom umgebenden Kontext, was es einfacher macht, auf das äußere this zuzugreifen.

javascript
function Person() {
  this.age = 0;

  setInterval(() => {
    // `this` bezieht sich auf die Person-Instanz
    this.age++;
  }, 1000);
}
Arrow, Lambda, Anonyme Funktion

Arrow Functions sind auch bekannt als Lambda Expression oder Anonyme Funktionen. Jedoch können anonyme Funktionen auch mit function erstellt werden..

JS32 Ausnahmen

Es gibt Fälle, in denen traditionelle Funktionen (function) bevorzugt werden, z.B. wenn this explizit benötigt wird oder wenn arguments verwendet werden soll.

javascript
const sum = function() {
  return Array.from(arguments).reduce((a, b) => a + b, 0);
};

me = this kann verwendet werden, wenn this in einer Funktion verwendet wird, die in einem anderen Kontext ausgeführt wird. Beispielsweise durch ein bind oder call, worin das this-Objekt überschrieben wird.

javascript

function outerKontext() {
    const me = this;

    const kontextGebundeneFunction = function() {
        console.log(this); // == me
        me.doSomething(); // == this von outer()
    }.call(otherThis);
}

JS33 Ternärer Operator

Der ternäre Operator (condition ? expression1 : expression2) soll verwendet werden, um einfache Bedingungen in einer einzigen Zeile zu schreiben. Er ist einfach zu lesen und zu schreiben. Er soll jedoch nicht geschachtelt werden, um die Lesbarkeit zu erhalten. Verwende dann lieber if...else.

javascript
const result = condition ? expression1 : expression2;

Hinweis

Der ternäre Operator ist auch bekannt als bedingter Operator oder Elvis Operator.

Ternär bedeutet, dass der Operator drei Operanden hat: die Bedingung, den Ausdruck, der ausgeführt wird, wenn die Bedingung wahr ist, und den Ausdruck, der ausgeführt wird, wenn die Bedingung falsch ist.

Komplexität

  • Der ternäre Operator soll nicht verschachtelt werden, um die Lesbarkeit zu erhalten.
  • Der ternäre Operator soll nur für kurze Ausdrücke verwendet werden.
  • Bei komplexeren Bedingungen oder Ausdrücken ist es besser, if...else zu verwenden.
  • Bei komplexeren Bedingungen oder Ausdrücken kann auch eine separate Funktion verwendet werden.

JS34 Array Prototype Methoden

Der Einsatz von Array-Methoden wie map(), filter(), reduce(), find(), every(), some() und anderen soll bevorzugt werden, um Arrays zu durchlaufen und Operationen auf den Elementen durchzuführen, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.

JS34 Problem

Oftmals werden Schleifen verwendet, um Arrays zu durchlaufen und Operationen auf den Elementen durchzuführen. Dies ist jedoch umständlich und unübersichtlich.

JS34 Lösung

Array-Methoden wie map(), filter(), reduce(), find(), every(), some() und andere bieten eine elegante und effiziente Möglichkeit, Arrays zu durchlaufen und Operationen auf den Elementen durchzuführen.

MethodeErklärungBeispiel
map()Erstellt ein neues Array, indem eine Funktion auf jedes Element angewendet wird.const doubled = [1, 2, 3].map(x => x * 2); // [2, 4, 6]
filter()Erstellt ein neues Array, das nur die Elemente enthält, für die die Funktion true zurückgibt.const evens = [1, 2, 3, 4].filter(x => x % 2 === 0); // [2, 4]
reduce()Wendet eine Funktion auf jedes Element des Arrays an, um ein einzelnes Ergebnis zu erzeugen.const sum = [1, 2, 3].reduce((acc, x) => acc + x, 0);// 6
find()Gibt das erste Element im Array zurück, für das die Funktion true zurückgibt.const even = [1, 2, 3].find(x => x % 2 === 0); // 2
every()Gibt true zurück, wenn die Funktion für jedes Element true zurückgibt.const allEven = [2, 4, 6].every(x => x % 2 === 0); // true
some()Gibt true zurück, wenn die Funktion für mindestens ein Element true zurückgibt.const hasEven = [1, 2, 3].some(x => x % 2 === 0); // true
findIndex()Gibt den Index des ersten Elements zurück, für das die Funktion true zurückgibt.const index = [1, 2, 3].findIndex(x => x % 2 === 0); // 1
indexOf()Gibt den Index des ersten Vorkommens eines Elements im Array zurück.const index = [1, 2, 3].indexOf(2); // 1
slice()Gibt eine flache Kopie eines Teils des Arrays zurück.const copy = [1, 2, 3].slice(1, 2); // [2]
splice()Ändert den Inhalt eines Arrays durch das Entfernen oder Ersetzen vorhandener Elemente und/oder das Hinzufügen neuer Elemente.const removed = [1, 2, 3].splice(1, 1); // [2]
entries()Gibt ein Array-Iterator-Objekt zurück, das Schlüssel/Wert-Paare für jedes Index/Element im Array enthält.for (const [index, value] of ['a', 'b'].entries()) { console.log(index, value); }
keys()Gibt ein Array-Iterator-Objekt zurück, das die Schlüssel für jeden Index im Array enthält.for (const index of ['a', 'b'].keys()) { console.log(index); }
values()Gibt ein Array-Iterator-Objekt zurück, das die Werte für jeden Index im Array enthält.for (const value of ['a', 'b'].values()) { console.log(value); }

Verkettung von Methoden

Operatoren können auch mit den Methoden kombiniert werden. Beachte jedoch, dass die Ausdrücke nicht zu komplex werden sollen, um die Lesbarkeit zu erhalten.

javascript
const sum = [1, 2, 3]
    .filter(x => x % 2 === 1)
    .map(x => x * 2)
    .reduce((acc, x) => acc + x, 0); // 8

Du kannst auch Methoden-Referenzen verwenden, um die Lesbarkeit zu verbessern.

javascript
const divisibleByTwo = x => x % 2 === 0;
const addSum = (acc, x) => acc + x;

const sum = [1, 2, 3]
    .filter(divisibleByTwo)
    .map(x => x * 2)
    .reduce(addSum, 0); // 6

JS35 Import/Export

Der import- und export-Mechanismus soll verwendet werden, um Module in JavaScript zu organisieren und zu verwalten.

JS35 Benannter Export

Im folgenden Beispiel wird ein Export mit einem Namen definiert.

javascript
// export.js
export const sum = (a, b) => a + b;
export const VALUE = 42;

// import.js
import { sum, VALUE } from './export.js';

JS35 Default Export

Im folgenden Beispiel wird ein Export als Standard definiert.

javascript
// export.js
const sum = (a, b) => a + b;
export default sum;

// import.js
import mySum from './export.js';

Beachte:

  • Der importierte Name kann beliebig sein (mySum).
  • Benannter und default Export können in einem Modul kombiniert werden.
javascript
// export.js
export const sum = (a, b) => a + b;
export default const anotherSum = (a, b) => a + b;

// import.js
import mySum, { sum } from './export.js';

JS35 Namespace-Import

Namespace-Import ermöglicht es, alle Exporte eines Moduls in einem Objekt zu importieren.

javascript
// export.js
export const sum = (a, b) => a + b;
export const VALUE = 42;

// import.js
import * as myExports from './export.js';
console.log(myExports.sum(1, 2)); // 3
console.log(myExports.VALUE); // 42

JS35 Aliase-Import

Du kannst auch Aliase für benannte Importe verwenden.

javascript
import { sum as add, VALUE as NUMBER } from './export.js';

JS36 Einsatz von Set und Map statt Arrays und Objekten

Seit ECMAScript 6 (ES6) gibt es die Datentypen Set und Map, die für die Verwaltung eindeutiger Werte und Schlüssel-Wert-Paare verwendet werden können.

Map ist ein Schlüssel-Wert-Datentyp, der es ermöglicht, Schlüssel und Werte zu speichern und abzurufen, ähnlich wie ein Objekt. Set ist ein Datentyp, der es ermöglicht, eindeutige Werte zu speichern und abzurufen, ähnlich wie ein Array mit eindeutigen Werten oder ein Objekt mit eindeutigen Schlüsseln.

Verwende Set und Map, um eindeutige Werte und Schlüssel-Wert-Paare effizient und einfach zu verwalten, anstatt Arrays und Objekte zu verwenden.

JS36 Problem

Arrays und Objekte werden oft für die Verwaltung von eindeutigen Werten und Schlüssel-Wert-Paaren verwendet. Dies ist jedoch oftmals aufwändig und unübersichtlich und in anderen Sprachen gibt es spezielle Datentypen für diese Zwecke. Oftmals entwickeln Entwickler eigene Lösungen, um eindeutige Werte oder Schlüssel-Wert-Paare zu verwalten.

JS36 Lösung

Die Verwendung von Set und Map ermöglicht eine effiziente und einfache Verwaltung von eindeutigen Werten und Schlüssel-Wert-Paaren.

JS36 Set

javascript
const uniqueNumbers = new Set([1, 2, 3, 1, 2, 3]);
console.log(uniqueNumbers); // Set { 1, 2, 3 }

JS36 Map

javascript
const keyValuePairs = new Map([
    ['key1', 'value1'],
    ['key2', 'value2'],
    ['key3', 'value3']
]);
console.log(keyValuePairs); // Map { 'key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3' }
Komplexe Schlüssel

Maps können als Schlüssel, im Gegensatz zu Objekten, auch Objekte, Funktionen und andere komplexe Datentypen verwenden.

javascript
const complexMap = new Map([
    [{ a: 1 }, 'value1'],
    [{ b: 2 }, 'value2'],
    [{ c: 3 }, 'value3']
]);
console.log(complexMap); // Map { { a: 1 } => 'value1', { b: 2 } => 'value2', { c: 3 } => 'value3' }

JS37 Abschließende Kommas

Trailing Commas sollen verwendet werden, um die Wartbarkeit des Codes zu erhöhen.

Trailing Commas sind Kommata, die am Ende von Arrays, Objekten, Funktionsparametern und Argumenten stehen.

JS37 Problem

Das einfache und schnelle Hinzufügen oder Entfernen von Elementen oder Eigenschaften in Arrays oder Objekten kann zu Fehlern wegen fehlendem vorangestellten oder nachgestellten Komma führen.

Im folgenden Beispiel kann das Hinzufügen eines neuen Elements zu einem Array zu einem Fehler führen, wenn das Komma fehlt.

javascript
const numbers = [
    { langerUndKomplexesObjekt: 'a' },
    { langerUndKomplexesObjekt: 'b' }   // Kein Komma am Ende
    // Füge neues Element hinzu
    { langerUndKomplexesObjekt: 'c' } // Fehler, Komma fehlt
];

JS37 Lösung

Trailing Commas können verwendet werden, um das Hinzufügen oder Entfernen von Elementen oder Eigenschaften in Arrays oder Objekten zu erleichtern.

javascript
const numbers = [
    { langerUndKomplexesObjekt: 'a' },
    { langerUndKomplexesObjekt: 'b' },  // Abschließendes Komma
];

// Füge neues Element hinzu
const numbers = [
    { langerUndKomplexesObjekt: 'a' },
    { langerUndKomplexesObjekt: 'b' },
    { langerUndKomplexesObjekt: 'c' },
];

Unterstützter Syntax

Trailing Commas sind in Arrays, Objekten, Funktionsparametern, Funktionsargumenten und Import- und Export-Anweisungen sowie in Destrukturierungen erlaubt.

JSON und Trailing Commas

Trailing Commas sind in JSON nicht erlaubt und können zu Syntaxfehlern führen.

JS38 for, forEach, for of, for in

In JavaScript gibt es verschiedene Möglichkeiten zu iterieren.

  • for-Schleife ist die ursprüngliche Schleife
  • forEach-Methode für Arrays mit Callback-Funktion
  • for...of-Schleife für iterierbare Objekte (Arrays, Strings, Sets, Maps)
  • for...in-Schleife für Objekte

Es soll die Methode verwendet werden, die am besten zur Aufgabe passt, um die Lesbarkeit und Wartbarkeit des Codes zu verbessern.

Vermische jedoch die Methoden nicht im gleichen (Funktions- oder Modul)Kontext, wenn es nicht unbedingt notwendig ist.

JS38 for-Schleife

Die ursprüngliche for-Schleife ist die am meisten verwendete Schleife in JavaScript. Sie besteht aus drei Teilen: Initialisierung, Bedingung und Inkrementierung/Dekrementierung.

javascript
for (let i = 0; i < 5; i++) {
    console.log(i);
}

Veraltet

Diese Schleife soll nur noch in Ausnahmefällen verwendet werden, wenn über ein Index ein Feld durchlaufen wird und beispielsweise der Index für Berechnungen benötigt wird.

Die for...of-Schleife ist andernfalls einfacher zu lesen.

Objekte

Die for-Schleife kann auch für Objekte verwendet werden, um über die Schlüssel zu iterieren. Es ist zu beachten, dass die Reihenfolge der Schlüssel nicht garantiert ist und auch Schlüssel aus dem Prototypen iteriert werden. Deshalb muss mit hasOwnProperty geprüft werden, ob der Schlüssel direkt im eigenen Objekt vorhanden ist. Andernfalls werden versehentlich auch Schlüssel aus dem Prototypen iteriert.

Nutze besser die for...in-Schleife.

Unendliche Schleife

Die Schleife kann unendlich laufen, wenn die End-Bedingung nicht korrekt ist.

javascript

for (let i = 0; i < 5; i--) {
    console.log(i);
}

JS38 forEach-Methode

Die forEach-Methode wird auf Arrays verwendet und führt eine Funktion für jedes Element im Array aus.

javascript
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
    console.log(number);
});
// oder kürzer mit Methodenreferenz
numbers.forEach(console.log);

Einsatzgebiet

Die forEach-Methode existiert für die Prototypen von Array, Map, Set, String und einigen mehr.

JS38 for...of-Schleife

Die for...of-Schleife (neu seit ES6) ist eine moderne Schleife, die für Arrays, Strings, Sets, Maps und andere iterierbare Objekte verwendet wird.

javascript
const numbers = [1, 2, 3, 4, 5];

for (const number of numbers) {
    console.log(number);
}

Es kann auch Destrukturierung verwendet werden.

javascript
const numbers = [[1, 2], [3, 4], [5, 6]];

for (const [first, second] of numbers) {
    console.log(first, second);
}

Objekte

Die for...of-Schleife kann nicht direkt auf Objekte verwendet werden, da diese nicht iterierbar sind.

JS38 for...in-Schleife

Die for...in-Schleife wird verwendet, um über die Schlüssel (und Werte) eines Objekts zu iterieren.

javascript
const person = {
    name: 'Alice',
    age: 30
};

for (const key in person) {
    console.log(key, person[key]);
}

Mit Destrukturierung:

javascript
for (const [key, value] of Object.entries(person)) {
    console.log(key, value);
}

JS39 Methoden-Verkettung

Die Methoden-Verkettung soll verwendet werden, um Methodenaufrufe auf einem Objekt in einer einzigen Anweisung zu verkettet.

Methoden-Verkettung ist eine Technik, bei der mehrere Methodenaufrufe auf einem Objekt in einer einzigen Anweisung verkettet werden. Dies wird beispielsweise bei Array-Methoden wie map(), filter(), reduce() und anderen verwendet.

Verwende Methoden-Verkettung, um den Code kompakter und lesbarer zu machen.

JS39 Beispiel

javascript
const numbers = [1, 2, 3, 4, 5];

const sum = numbers
    .filter(x => x % 2 === 1)
    .map(x => x * 2)
    .reduce((acc, x) => acc + x, 0);

JS39 Regeln

  • Jeder Methodenaufruf wird auf einer neuen Zeile eingerückt (entsprechend den ESLint-Regeln).
  • Jeder Methodenaufruf wird durch einen Punkt (.) vorangehend zum Methodennamen getrennt.
  • Verschachtelung werden vermieden, um die Lesbarkeit zu erhalten, ggf. durch Methoden-Referenzen.
javascript
const sum = numbers
    .filter(divisibleByTwo)
    .map(double)
    .reduce(addSum, 0);

JS39 Vorteile

  • Kompakter und lesbarer Code
  • Einfache Verkettung von Methodenaufrufen
  • Bessere Performance durch Vermeidung von Zwischenvariablen
  • Einfache Wiederverwendung von Methodenketten

JS39 Ausnahmen

  • Übermäßige Verkettung von Methoden kann die Lesbarkeit beeinträchtigen.
  • Bei komplexen Operationen oder Bedingungen ist es besser, die Methodenaufrufe aufzuteilen.
  • Bei der Verkettung von Methoden ist darauf zu achten, dass die Reihenfolge der Methodenaufrufe korrekt ist.

JS40 Unbenutzte Variablen und Parameter

Es sollen keine unbenutzten Variablen und Parameter im Code vorhanden sein.

  • Wenn die Funktionsdeklaration die Parameter vorschreibt, kann _ als Platzhalter für unbenutzte Parameter verwendet werden.
  • Mehrere unbenutzte Parameter können durch (_, __, ___) etc. gekennzeichnet werden.
Linter bei Verwendung des Unterstrichs

Gegebenenfalls wird ein Linter bei der Verwendung des Unterstrichs als Platzhalter für unbenutzte Variablen oder Parameter eine Warnung ausgeben. Dies kann in der Konfiguration des Linters deaktiviert werden.

Von Stack Overflow

JSON
// .eslintrc.json
{
  // ...
  "rules": {
    // note you must disable the base rule
    // as it can report incorrect errors
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": [
      "warn", // or "error"
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_"
      }
    ]
  }
}

JS40 Problem

Unbenutzte Variablen und Parameter können zu Verwirrung und unerwartetem Verhalten führen.

Das Entfernen von unbenutzten Parametern ist jedoch auch nicht möglich, wenn die Parameter vorne deklariert sind, da dies zu einem Fehler führen würde.

Vererbung und Interfaces können auch unbenutzte Parameter erzeugen.

JS40 Lösung

  • Entferne alle Parameter, wenn keiner davon benutzt wird.
  • Entferne den Parameter, wenn er eindeutig unbenutzt ist.
  • Verwende _ als Platzhalter für unbenutzte Variablen und Parameter, um den Code sauber zu halten.
javascript

function sum(a, b, _) {
    return a + b;
}

JS40 Vorteile

  • Sauberer und wartbarer Code
  • Vermeidung von Verwirrung und unerwartetem Verhalten
  • Bessere Lesbarkeit und Verständlichkeit des Codes

JS40 Nachteile

  • Der Unterstrich kann zu Verwirrung führen, wenn er nicht als Platzhalter für unbenutzte Variablen oder Parameter verwendet wird.
  • Spätere Erweiterungen der Funktion oder Methode lassen den Namen des originalen Parameters vermissen, wenn der Unterstrich verwendet wird. Bitte beachten, dass eine Erweiterung einer vorhandenen Methode gegen das OCP Prinzip verstößt.

JS40 Ausnahmen

  • Bei bereits vorhandene Funktionen oder Methoden besteht die Gefahr, dass das Entfernen eines Parameters und damit einer semantischen Änderung der Reihenfolge der Parameter zu Fehlern beim Aufruf von vorhandenen Code führt.
javascript
function original(unusedParameter1, parameter2, parameter3) {
    // ...
}

function refactored(parameter2, parameter3) {
    // ...
}

// Aufruf irgendwo

original(1, 2, 3); // Fehler

JS41 Verwende spezielle Objekte statt spezielle Werte

Wenn Objekte, wie User oder jede andere Art von Entität verwendet werden, und es spezielle Fälle gibt wie nicht gefunden, ungültig, leer, fehlerhaft, etc., dann sollen spezielle abgeleitete Objekte verwendet werden, um diese Fälle zu repräsentieren.

JS41 Problem

Spezielle Fälle wie nicht gefunden, ungültig, leer, fehlerhaft, etc. werden oft durch spezielle Werte wie null, undefined, -1, 0, '', false, etc. repräsentiert. Dies führt dazu, dass im Code ständig überprüft werden muss, ob der Wert speziell ist und entsprechend behandelt werden muss.

Wird diese Prüfung nicht gemacht und vergessen, kommt es zu Fehlern wie Null-Pointer-Exceptions, undefined is not a function, TypeError: Cannot read property '...' of null, etc. Diese Fehler sind schwer zu finden und zu beheben, da sie oft an einer anderen Stelle im Code auftreten, als wo der Fehler tatsächlich liegt.

javascript
function getUser(id) {
  const user = getUserFromDatabase(id);

  if (user == null) {
    return null;
  }

  return user;
}

JS41 Lösung

Verwende abgeleitete Objekte, um spezielle Fälle zu repräsentieren. Es kann beispielsweise ein NotFoundUser-Objekt für den Fall eines nicht-gefundenen Benutzers erstellt werden.

Dieses leere Benutzer-Objekt verhält sich anders im Vergleich zu einem korrekten Benutzer-Objekt. So können damit keine Operationen durchgeführt werden, die nur für korrekte Benutzer erlaubt sind.

Die Prüfung auf einen nicht-gefundenen Benutzer kann durch Methoden des Objekts selbst erfolgen. Sollte dieses Objekt doch einmal verwendet werden, so gibt nur dann eine Exception, wenn die Operation am Objekt nicht erlaubt ist. Die Gültigkeit wird in operativen Methoden geprüft (siehe Trennung von operationalem und integrativem Code), so können Integrationsmethoden diese Werte einfach weitergeben.

Das folgende Beispiel zeigt die Verwendung eines speziellen Objekts NotFoundEntity für den Fall, dass eine Entität (ein generisches Beispiel-Daten-Objekt) nicht gefunden wurde. Es werden keine Exceptions geworfen, sondern spezielle Objekte zurückgegeben, die spezielles Verhalten haben (Polymorphismus). Wenn diese Objekte verwendet werden, wird das spezielle Verhalten automatisch ausgeführt. In diesem Fall wird ein leeres Array zurückgegeben. Alternativ kann auch ein Fehler geworfen werden, wenn das spezielle Objekt verwendet wird.

Kurzgesagt

Wenn spezielle Objekte verwendet werden, um spezielle Fälle zu repräsentieren, kann damit trotzdem gearbeitet werden und das Ergebnis ist immer gültig.

Optional

Das spezielle Objekt Optional kann auch verwendet werden, um diese spezielle Fälle zu repräsentieren. Es ist nützlich, wenn bereits Klassen und Objekte aus einer Legacy-Anwendung verwendet werden, die nicht geändert werden können.

Siehe auch Null-Objekte.

javascript

class NotFoundEntity extends Entity {
  static create() {
    // schützt vor Veränderung des Objekts
    return Object.freeze(new NotFoundEntity())
  }
  constructor() {
    super(-1, 'Unknown');
  }
  isValid() {
    return false;
  }
}

class Entity {
  static NOT_FOUND = NotFoundEntity.create();

  isValid() {
    return true;
  }
  // ...
}

// Methode braucht kein Exceptionhandling
function getEntityById(id) {
  if (id === 1) {
    return new Entity(1, 'Alice'); //oder Erstellung über Factory
  }
  // statt Exception
  return Entity.NOT_FOUND; // oder return Optional.empty();
}

function linesOfEntity(entity) {
  if (!entity.isValid()) {
    return new EmptyFoo();
  }
  return new Foo(entity);
}

function dataOfEntity(lines) {
  return new Data(lines.data);
}

function dataToArray(data) {
  return data.map(d => d.value);
}

function foo() {
  const entity = getEntityById(1);
  const linesOfEntity = linesOfEntity(entity);
  const data = dataOfEntity(linesOfEntity);

  return dataToArray(data);
}

// Alternativ mit Exception
function fooStrict() {
  const entity = getEntityById(1);

  if (!entity.isValid()) {
    throw new Error('Entity not found');
  }

  const linesOfEntity = linesOfEntity(entity);
  const data = dataOfEntity(linesOfEntity);

  return dataToArray(data);
}

JS41 Vorteile

  • Keine Null-Pointer-Exceptions
  • Spezielle Fälle werden explizit repräsentiert.
  • Unterscheidung zwischen verschiedenen Fällen durch unterschiedliches Verhalten und Objekte (statt null)
  • Keine ständige Überprüfung auf spezielle Werte notwendig (wie null, undefined, -1, 0, '', false, etc.)
  • Code kann nicht fehlschlagen, weil keine spezielle Werte verwendet werden.
  • Kein Exceptionhandling
    • Vermeidet verschachtelte try-catch-Blöcke
    • Testen von speziellem Verhalten wird einfacher oder braucht gar nicht mehr getestet zu werden, da es nichts zu testen gibt
    • API wird einfacher, da keine Exceptions geworfen werden müssen und Rückgabewerte immer gültig und prüfbar (isValid()) sind
  • Code wird einfacher und lesbarer, da spezielle Fälle keine zusätzlichen if-Anweisungen benötigen.

JS41 Nachteile

  • Architektur der Klassen und Objekte wird komplexer oder vorhandene Architektur muss angepasst werden.
  • Methoden müssen in ihrer Dokumentation nun statt Exceptions spezielle Objekte beschreiben.
  • Spezielle Objekte müssen erstellt und gepflegt werden.
  • Spezielle Objekte können zu einer größeren Anzahl von Klassen führen.
  • Umstellung bestehender Code kann aufwändig sein.
  • Exceptions sind in Fleisch und Blut der meisten Entwickler und werden oft als einfacher angesehen.
  • Performance kann durch die Erstellung von speziellen Objekten beeinträchtigt werden, insbesondere da Pfade nicht mehr durch Prüfung von speziellen Werten abgekürzt werden könnten.

Anderes Vorgehen gleiche Wirkung

Der Einsatz von speziellen Werten wie null und undefined unterscheidet sich im Endergebnis nicht von speziellen Objekten. Eine Prüfung muss früher oder später erfolgen, ob es sich um einen speziellen Fall handelt (null oder isValid()).

Jedoch ist in der Entwicklung oft die Situation gegeben, dass Entwickler einen Eingabewert nicht prüfen und es dadurch zu Fehlern kommt. Durch den Einsatz von speziellen Objekten wird die Prüfung auf spezielle Werte automatisiert und der Code wird sicherer. Erst sogenannte Kernfunktionen, die die speziellen Objekte verwenden, müssen die Prüfung dann durchführen. Diese sind in der Regel besser getestet im Gegensatz zu den weiter oben in der Hierarchie liegenden Methoden.

Durch den Einsatz von speziellen Objekten wird es unwahrscheinlicher, dass Fehler wie null-Pointer-Exceptions oder undefined-Exceptions auftreten.

JS42 Lokale Funktionen verwenden

Lokale Funktionen innerhalb einer Funktion sollen verwendet werden, um Code zu strukturieren, Code-Blöcke zu extrahieren und in der Nähe ihrer Verwendeung (Gesetz von Demeter) zu halten.

JS42 Problem

Funktionen werden oft in einer Datei oder einem Modul definiert und dann an verschiedenen Stellen im Code verwendet. Dies führt dazu, dass der Code unübersichtlich wird und die Funktionen nicht in der Nähe ihrer Verwendung sind.

Weiterhin können solche Funktionen/Methoden in Klassen nicht als privat gekennzeichnet werden bzw. müssen dann im Konstruktor definiert werden, was den Konstruktor aufbläht.

javascript

class MyClass {
  constructor() {
    this.myFunction = () => {
      // ...
    };
    this.weitereFunktion = () => {
      // ...
    };
  }


  anotherFunction() {
    this.myFunction();
  }
}

JS42 Lösung

Stattdessen sollen Funktionen innerhalb einer Funktion als Konstante definiert werden.

  • Innere Funktionen sollen an den Anfang der Funktion definiert werden.
  • Innere Funktionen sollen als Konstanten definiert werden.
  • Innere Funktionen sollen als Arrow-Funktionen definiert werden, um den Kontext der äußeren Funktion zu erhalten.
  • Innerhe Funktionen sollen nicht als Callbacks verwendet werden.
javascript
class MyClass {
    anotherFunction() {
      const myFunction = () => {
        // ...
      };
      const weitereFunktion = () => {
        // ...
      };

      myFunction();
    }
  }
}

Achtung

Wenn die übergreifende Funktion zu groß wird, sollte sie in kleinere Funktionen aufgeteilt werden, auch wenn damit in JavaScript die Methoden sichtbar werden und das Gesetz von Demeter verletzt wird.

JS42 Vorteile

  • Code wird strukturiert und lesbarer
  • Funktionen sind in der Nähe ihrer Verwendung
  • Funktionen sind nur innerhalb der Funktion sichtbar

JS42 Nachteile

  • Funktionen können nicht von außen getestet werden
  • Funktionen können nicht von anderen Funktionen verwendet werden
  • Die übergeordnete Funktion wird größer, wenn viele lokale Funktionen verwendet werden.

JS43 setTimeout und setInterval

Die globalen Funktion setTimeout und setInterval werden in JavaScript oft verwendet, um Code zu verzögern oder in regelmäßigen Abständen auszuführen. Die Verwendung von setTimeout und setInterval soll vermieden werden, aus den folgenden Gründen:

  1. Asynchronität: setTimeout und setInterval sind asynchron und führen zu unvorhersehbarem Verhalten, wenn sie mit anderen asynchronen Operationen kombiniert werden.
  2. Race Conditions: setTimeout und setInterval können zu Zeitpunkten ausgeführt werden, die mit anderen Operationen kollidieren, was zu sogenannten "Heisenbugs" führen kann.

JS43 Alternativen

  1. Promises: Verwende Promises, um asynchrone Operationen zu verwalten.
  2. EventEmitter: Verwende EventEmitter, um benutzerdefinierte Ereignisse zu erstellen und zu verwalten.
  3. Async/Await: Verwende async/await, um asynchrone Operationen zu verwalten.
  4. Web Workers: Verwende Web Workers, um asynchrone Operationen im Hintergrund auszuführen.
  5. Dependency Injection: Verwende Dependency Injection.

JS43 Dependency Injection

Muss setTimeout oder setInterval verwendet werden, sollen die Funktionen setTimeout und setInterval als Abhängigkeiten über Dependency Injection injiziert werden. Damit kann die Abhängigkeit beispielweise in Tests durch eine Mock-Funktion ersetzt werden.

javascript

class MyClass {
  constructor(setTimeout, setInterval) {
    this.setTimeout = setTimeout;
    this.setInterval = setInterval;
  }

  myFunction() {
    this.setTimeout(() => {
      // ...
    }, 1000);

    this.setInterval(() => {
      // ...
    }, 1000);
  }
}
javascript

function myFunction(vars, { setTimeout, setInterval }) {
  setTimeout(() => {
    // ...
  }, 1000);

  setInterval(() => {
    // ...
  }, 1000);
}

JS43 Deinstallieren

Wenn setTimeout oder setInterval verwendet werden, soll die Funktion clearTimeout oder clearInterval verwendet werden, um die Zeitüberschreitung oder das Intervall zu deinstallieren.

javascript
const timeout = setTimeout(() => {
  // ...
}, 1000);

clearTimeout(timeout);

const interval = setInterval(() => {
  // ...
}, 1000);

clearInterval(interval);

JS44 Nebeneffekte vermeiden

Methoden und Funktionen sollten keine Nebeneffekte haben, die nicht offensichtlich sind. Dies verbessert die Vorhersagbarkeit, Wartbarkeit und Testbarkeit des Codes.

JS44 Problem

Nebeneffekte sind unerwünschte Veränderungen des Zustands eines Systems, die durch eine Funktion oder Methode verursacht werden. Sie können schwer zu erkennen und zu debuggen sein, da sie nicht offensichtlich sind und an einer anderen Stelle im Code auftreten können.

Einige Beispiele für unerwünschte Nebeneffekte:

JS44 1. Veränderung eines Objekts innerhalb einer Funktion

Im folgenden Beispiel wird die Funktion addValue definiert, die einen Wert zu einem Objekt hinzufügt. Diese Funktion verändert jedoch direkt den übergebenen Parameter und hat damit einen Nebeneffekt:

javascript
function addValue(obj) {
  obj.value = 42;  // Nebeneffekt: Das Original-Objekt wird verändert
}

JS44 2. Veränderung des internen Zustands einer Klasse

Die folgende Klasse enthält eine Methode getValue, die einen Wert berechnen soll. Allerdings verändert sie dabei eine interne Variable (this.result), was zu unerwarteten Seiteneffekten führen kann:

javascript
class Calculator {
  constructor() {
    this.result = 0;
  }

  getValue(value) {
    this.result += value; // Nebeneffekt: Interner Zustand wird verändert
    return this.result;
  }
}

JS44 3. Veränderung einer globalen Variable

Das folgende Beispiel zeigt eine Funktion, die eine globale Variable verändert:

javascript
function increment() {
  global.globalObject.value++; // Nebeneffekt: Änderung einer globalen Variable
}

JS44 Lösung

Um Nebeneffekte zu vermeiden, sollten Funktionen und Methoden so gestaltet werden, dass sie keine externen Zustände verändern. Stattdessen sollte mit Kopien gearbeitet oder Werte als Rückgabe geliefert werden.

JS44 1. Arbeiten mit unveränderlichen Objekten

Statt das Originalobjekt zu modifizieren, sollte eine neue Kopie mit den geänderten Werten zurückgegeben werden:

javascript
function addValue(obj) {
  return { ...obj, value: 42 }; // Keine Änderung am Original-Objekt
}

JS44 2. Vermeidung von Zustandsänderungen in Klassen

Methoden sollten keine internen Variablen verändern, sondern den neuen Wert zurückgeben:

javascript
class Calculator {
  constructor() {
    this.result = 0;
  }

  calculate(value) {
    return this.result + value; // Kein Nebeneffekt
  }
}

JS44 3. Funktionale Programmierung statt Mutation

Statt eine globale Variable zu ändern, sollte eine Funktion den neuen Wert berechnen und zurückgeben:

javascript
function increment(value) {
  return value + 1;
}

JS44 4. Dependency Injection statt globaler Variablen

Globale Variablen sollten vermieden werden. Stattdessen kann Dependency Injection genutzt werden, um Abhängigkeiten explizit zu übergeben:

javascript
class GlobalObject {
  constructor(dependency) {
    this.dependency = dependency;
  }

  foo() {
    this.dependency.increment();
  }
}

const globalObject = { value: 0, increment: function() { this.value++; } };
new GlobalObject(globalObject).foo();

JS44 Fazit

  • Vermeide direkte Modifikationen von Objekten oder globalen Variablen.
  • Verwende unveränderliche (immutable) Datenstrukturen.
  • Bevorzuge reine Funktionen, die keine Nebeneffekte haben.
  • Nutze Dependency Injection, um Abhängigkeiten explizit zu machen.