Anhang

Prototypische Vererbung

Mit Ausnahme von null und undefined hat jeder primitive Datentyp einen Prototyp, einen entsprechenden Objekt-Wrapper, der Methoden zum Arbeiten mit Werten bereitstellt. Wenn eine Methode oder Property-Suche für ein primitives Objekt aufgerufen wird, umhüllt JavaScript das primitive Objekt im Hintergrund und ruft die Methode oder führt die Property-Suche stattdessen für das Wrapper-Objekt aus.

Ein Stringliteral hat beispielsweise keine eigenen Methoden, aber Sie können die Methode .toUpperCase() über den entsprechenden String-Objekt-Wrapper darauf anwenden:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Dies wird als prototypische Vererbung bezeichnet – das Übernehmen von Eigenschaften und Methoden aus dem entsprechenden Konstruktor eines Werts.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object {  }

Sie können mit diesen Konstruktoren primitive Typen erstellen, anstatt sie nur anhand ihres Werts zu definieren. Mit dem Konstruktor String wird beispielsweise ein Stringobjekt und kein Stringliteral erstellt: ein Objekt, das nicht nur unseren Stringwert, sondern auch alle geerbten Eigenschaften und Methoden des Konstruktors enthält.

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

Die resultierenden Objekte verhalten sich größtenteils wie die Werte, mit denen sie definiert wurden. Auch wenn die Definition eines Zahlenwerts mit dem new Number-Konstruktor beispielsweise zu einem Objekt führt, das alle Methoden und Eigenschaften des Number-Prototyps enthält, können Sie auf diese Objekte mathematische Operatoren anwenden, genau wie auf numerische Literale:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

Sie müssen diese Konstruktoren nur sehr selten verwenden, da sie aufgrund der integrierten prototypischen Vererbung in JavaScript keinen praktischen Nutzen bieten. Das Erstellen von Primitivtypen mithilfe von Konstruktoren kann auch zu unerwarteten Ergebnissen führen, da das Ergebnis ein Objekt und kein einfaches Literal ist:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Dies kann die Verwendung strenger Vergleichsoperatoren erschweren:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Automatische Semikolon-Einfügung (Automatic Semicolon Insertion, ASI)

Beim Parsen eines Scripts können JavaScript-Interpreter die Funktion „Automatic Semicolon Insertion“ (ASI) verwenden, um fehlende Semikolons zu korrigieren. Wenn der JavaScript-Parser auf ein nicht zulässiges Token stößt, versucht er, vor diesem Token ein Semikolon einzufügen, um den potenziellen Syntaxfehler zu beheben, sofern eine oder mehrere der folgenden Bedingungen erfüllt sind:

  • Dieses Token ist durch einen Zeilenumbruch vom vorherigen Token getrennt.
  • Dieses Token ist }.
  • Das vorherige Token ist ) und das eingefügte Semikolon ist das abschließende Semikolon einer dowhile-Anweisung.

Weitere Informationen finden Sie in den ASI-Regeln.

Wenn Sie beispielsweise nach den folgenden Anweisungen Semikolons weglassen, führt dies aufgrund von ASI nicht zu einem Syntaxfehler:

const myVariable = 2
myVariable + 3
> 5

ASI kann jedoch nicht mehrere Anweisungen in derselben Zeile berücksichtigen. Wenn Sie mehrere Anweisungen in derselben Zeile eingeben, müssen Sie sie durch Semikolons trennen:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI ist ein Versuch zur Fehlerkorrektur und keine Art von syntaktischer Flexibilität, die in JavaScript integriert ist. Verwenden Sie Semikolons an den entsprechenden Stellen, damit Sie nicht darauf angewiesen sind, dass der Code korrekt ist.

Strenger Modus

Die Standards für die Programmierung von JavaScript haben sich weit über alles hinaus entwickelt, was bei der frühen Entwicklung der Sprache berücksichtigt wurde. Jede neue Änderung am erwarteten Verhalten von JavaScript darf keine Fehler auf älteren Websites verursachen.

ES5 behebt einige langjährige Probleme mit der JavaScript-Semantik, ohne bestehende Implementierungen zu beeinträchtigen. Dazu wird der „Strict Mode“ eingeführt, mit dem sich für ein gesamtes Script oder eine einzelne Funktion eine strengere Reihe von Sprachregeln aktivieren lässt. Wenn Sie den strengen Modus aktivieren möchten, verwenden Sie das Stringliteral "use strict" gefolgt von einem Semikolon in der ersten Zeile eines Scripts oder einer Funktion:

"use strict";
function myFunction() {
  "use strict";
}

Der strenge Modus verhindert bestimmte „unsichere“ Aktionen oder verworfene Funktionen, löst anstelle der gängigen „stummen“ Fehler explizite Fehler aus und verbietet die Verwendung von Syntaxen, die mit zukünftigen Sprachfunktionen in Konflikt stehen könnten. So erhöhten beispielsweise frühe Designentscheidungen zum Variablenbereich die Wahrscheinlichkeit, dass Entwickler den globalen Bereich versehentlich „verschmutzen“, wenn sie eine Variable deklarieren, unabhängig vom enthaltenden Kontext, indem sie das Schlüsselwort var weglassen:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

Moderne JavaScript-Laufzeitumgebungen können dieses Verhalten nicht korrigieren, ohne das Risiko, dass Websites, die darauf angewiesen sind, entweder versehentlich oder absichtlich beschädigt werden. Stattdessen wird dies in modernen JavaScript-Versionen verhindert, indem Entwickler den strengen Modus für neue Arbeit aktivieren können und er standardmäßig nur im Zusammenhang mit neuen Sprachfunktionen aktiviert wird, bei denen keine älteren Implementierungen beeinträchtigt werden:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

"use strict" muss als Stringliteral geschrieben werden. Ein Template-Literal (use strict) funktioniert nicht. Außerdem muss "use strict" vor jedem ausführbaren Code im vorgesehenen Kontext eingefügt werden. Andernfalls wird es vom Interpreter ignoriert.

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

Nach Verweis, nach Wert

Jede Variable, einschließlich Eigenschaften eines Objekts, Funktionsparameter und Elemente in einem Array, Set oder Map, kann entweder einen primitiven Wert oder einen Referenzwert enthalten.

Wenn ein primitiver Wert von einer Variablen einer anderen zugewiesen wird, erstellt die JavaScript-Engine eine Kopie dieses Werts und weist sie der Variablen zu.

Wenn Sie einer Variablen ein Objekt (Klasseninstanzen, Arrays und Funktionen) zuweisen, wird keine neue Kopie dieses Objekts erstellt. Stattdessen enthält die Variable einen Verweis auf die gespeicherte Position des Objekts im Arbeitsspeicher. Wenn Sie also ein Objekt ändern, auf das eine Variable verweist, ändert sich nicht nur der Wert dieser Variablen, sondern auch das Objekt selbst. Wenn Sie beispielsweise eine neue Variable mit einer Variablen initialisieren, die eine Objektreferenz enthält, und dann mit der neuen Variablen diesem Objekt eine Property hinzufügen, werden die Property und ihr Wert dem ursprünglichen Objekt hinzugefügt:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

Das ist nicht nur für das Ändern von Objekten, sondern auch für strenge Vergleiche wichtig. Denn für die strenge Gleichheit zwischen Objekten müssen beide Variablen auf dasselbe Objekt verweisen, damit das Ergebnis true ist. Sie können nicht auf verschiedene Objekte verweisen, auch wenn diese strukturell identisch sind:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Arbeitsspeicherzuweisung

JavaScript verwendet die automatische Speicherverwaltung. Das bedeutet, dass der Arbeitsspeicher während der Entwicklung nicht explizit zugewiesen oder deaktiviert werden muss. Die Details der Speicherverwaltung von JavaScript-Engines gehen über den Umfang dieses Moduls hinaus. Wenn Sie jedoch wissen, wie Speicher zugewiesen wird, können Sie Referenzwerte besser einsetzen.

Es gibt zwei „Bereiche“ im Arbeitsspeicher: den „Stack“ und den „Heap“. Im Stack werden statische Daten gespeichert, also primitive Werte und Verweise auf Objekte, da der feste Speicherplatz, der zum Speichern dieser Daten erforderlich ist, vor der Ausführung des Scripts zugewiesen werden kann. Im Heap werden Objekte gespeichert, die dynamisch zugewiesenen Speicherplatz benötigen, da sich ihre Größe während der Ausführung ändern kann. Der Arbeitsspeicher wird durch einen Prozess namens „Garbage Collection“ freigegeben, bei dem Objekte ohne Verweise aus dem Arbeitsspeicher entfernt werden.

Der Hauptthread

JavaScript ist eine Sprache mit einem einzigen Thread und einem „synchronen“ Ausführungsmodell. Das bedeutet, dass jeweils nur eine Aufgabe ausgeführt werden kann. Dieser sequenzielle Ausführungskontext wird als Hauptthread bezeichnet.

Der Haupt-Thread wird von anderen Browseraufgaben gemeinsam genutzt, z. B. zum Parsen von HTML, zum Rendern und erneuten Rendern von Seitenteilen, zum Ausführen von CSS-Animationen und zum Verarbeiten von Nutzerinteraktionen, von einfachen (z. B. Hervorheben von Text) bis hin zu komplexen (z. B. Interaktionen mit Formularelementen). Browseranbieter haben Möglichkeiten gefunden, die vom Hauptthread ausgeführten Aufgaben zu optimieren. Komplexere Scripts können jedoch immer noch zu viel der Ressourcen des Hauptthreads beanspruchen und sich auf die Gesamtleistung der Seite auswirken.

Einige Aufgaben können in Hintergrundthreads ausgeführt werden, die als Webworker bezeichnet werden. Dabei gelten jedoch einige Einschränkungen:

  • Worker-Threads können nur auf eigenständige JavaScript-Dateien zugreifen.
  • Sie haben keinen oder nur eingeschränkten Zugriff auf das Browserfenster und die Benutzeroberfläche.
  • Sie sind in der Kommunikation mit dem Haupt-Thread eingeschränkt.

Diese Einschränkungen machen sie ideal für fokussierte, ressourcenintensive Aufgaben, die sonst den Hauptthread belegen würden.

Der Aufrufstack

Die Datenstruktur, die zum Verwalten von „Ausführungskontexten“ verwendet wird – also des Codes, der gerade aktiv ausgeführt wird –, ist eine Liste namens Callstack (häufig auch nur „Stack“). Wenn ein Script zum ersten Mal ausgeführt wird, erstellt der JavaScript-Interpreter einen „globalen Ausführungskontext“ und fügt ihn dem Aufrufstack hinzu. Die Anweisungen in diesem globalen Kontext werden nacheinander von oben nach unten ausgeführt. Wenn der Interpreter bei der Ausführung des globalen Kontexts einen Funktionsaufruf findet, fügt er den Stack auf die Spitze einen „Funktionsausführungskontext“ für diesen Aufruf, hält den globalen Ausführungskontext an und führt den Funktionsausführungskontext aus.

Jedes Mal, wenn eine Funktion aufgerufen wird, wird der Ausführungskontext der Funktion für diesen Aufruf auf den Stapel gedrückt, direkt über dem aktuellen Ausführungskontext. Der Aufrufstapel funktioniert nach dem Prinzip „Letzter zuerst“. Das bedeutet, dass der letzte Funktionsaufruf, der sich am höchsten im Stapel befindet, ausgeführt wird und so lange fortgesetzt wird, bis er abgeschlossen ist. Wenn diese Funktion abgeschlossen ist, entfernt der Interpreter sie aus dem Aufrufstack. Der Ausführungskontext, der diesen Funktionsaufruf enthält, wird wieder zum obersten Element im Stack und die Ausführung wird fortgesetzt.

Diese Ausführungskontexte erfassen alle für die Ausführung erforderlichen Werte. Außerdem legen sie die Variablen und Funktionen fest, die im Gültigkeitsbereich der Funktion auf Grundlage des übergeordneten Kontexts verfügbar sind, und bestimmen und legen den Wert des Schlüsselworts this im Kontext der Funktion fest.

Die Ereignisschleife und die Rückrufwarteschlange

Diese sequenzielle Ausführung bedeutet, dass asynchrone Aufgaben, die Callback-Funktionen enthalten, z. B. das Abrufen von Daten von einem Server, das Reagieren auf Nutzerinteraktionen oder das Warten auf Timer, die mit setTimeout oder setInterval festgelegt wurden, entweder den Hauptthread blockieren, bis diese Aufgabe abgeschlossen ist, oder den aktuellen Ausführungskontext unerwartet unterbrechen, sobald der Ausführungskontext der Callback-Funktion dem Stack hinzugefügt wird. Um dies zu beheben, verwaltet JavaScript asynchrone Aufgaben mit einem ereignisgesteuerten „Parallelitätsmodell“, das aus dem „Ereignis-Loop“ und der „Callback-Warteschlange“ (manchmal auch als „Nachrichtenwarteschlange“ bezeichnet) besteht.

Wenn eine asynchrone Aufgabe im Hauptthread ausgeführt wird, wird der Ausführungskontext der Rückruffunktion nicht auf den Aufrufstapel gelegt, sondern in die Rückrufwarteschlange. Der Ereignis-Loop ist ein Muster, das manchmal als Reaktor bezeichnet wird. Er prüft kontinuierlich den Status des Aufrufstapels und der Rückrufwarteschlange. Wenn sich Aufgaben in der Rückrufwarteschlange befinden und die Ereignisschleife feststellt, dass der Aufrufstapel leer ist, werden die Aufgaben aus der Rückrufwarteschlange einzeln auf den Stapel geschoben, um ausgeführt zu werden.