JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

7 Syntax



7.1 Eine Übersicht über die JavaScript-Syntax

Dies ist ein erster Blick auf die JavaScript-Syntax. Machen Sie sich keine Sorgen, wenn einige Dinge noch keinen Sinn ergeben. Sie werden alle später in diesem Buch detaillierter erklärt.

Auch diese Übersicht ist nicht erschöpfend. Sie konzentriert sich auf das Wesentliche.

7.1.1 Grundlegende Konstrukte

7.1.1.1 Kommentare
// single-line comment

/*
Comment with
multiple lines
*/
7.1.1.2 Primitive (atomare) Werte

Booleans

true
false

Zahlen

1.141
-123

Der grundlegende Zahlentyp wird sowohl für Gleitkommazahlen (Doubles) als auch für Ganzzahlen verwendet.

Bigints

17n
-49n

Der grundlegende Zahlentyp kann Ganzzahlen nur innerhalb eines Bereichs von 53 Bits plus Vorzeichen korrekt darstellen. Bigints können beliebig groß werden.

Strings

'abc'
"abc"
`String with interpolated values: ${256} and ${true}`

JavaScript hat keinen separaten Typ für Zeichen. Es verwendet Strings, um sie darzustellen.

7.1.1.3 Assertions

Eine Assertion beschreibt, wie das Ergebnis einer Berechnung aussehen soll, und löst eine Ausnahme aus, wenn diese Erwartungen nicht erfüllt sind. Zum Beispiel besagt die folgende Assertion, dass das Ergebnis der Berechnung 7 plus 1 gleich 8 sein muss

assert.equal(7 + 1, 8);

assert.equal() ist ein Methodenaufruf (das Objekt ist assert, die Methode ist .equal()) mit zwei Argumenten: dem tatsächlichen Ergebnis und dem erwarteten Ergebnis. Es ist Teil einer Node.js Assertion API, die später in diesem Buch erklärt wird.

Es gibt auch assert.deepEqual(), das Objekte tief vergleicht.

7.1.1.4 Protokollierung auf der Konsole

Protokollierung auf der Konsole eines Browsers oder Node.js

// Printing a value to standard out (another method call)
console.log('Hello!');

// Printing error information to standard error
console.error('Something went wrong!');
7.1.1.5 Operatoren
// Operators for booleans
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or

// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);

// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);

// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');

// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);

JavaScript hat auch einen Vergleichsoperator ==. Ich empfehle, ihn zu vermeiden – warum, wird in §13.4.3 „Empfehlung: Immer strikte Gleichheit verwenden“ erklärt.

7.1.1.6 Variablen deklarieren

const erstellt unveränderliche Variablenbindungen: Jede Variable muss sofort initialisiert werden und wir können später keinen anderen Wert zuweisen. Der Wert selbst kann jedoch veränderlich sein und wir können seinen Inhalt ändern. Anders ausgedrückt: const macht Werte nicht unveränderlich.

// Declaring and initializing x (immutable binding):
const x = 8;

// Would cause a TypeError:
// x = 9;

let erstellt veränderliche Variablenbindungen

// Declaring y (mutable binding):
let y;

// We can assign a different value to y:
y = 3 * 5;

// Declaring and initializing z:
let z = 3 * 5;
7.1.1.7 Gewöhnliche Funktionsdeklarationen
// add1() has the parameters a and b
function add1(a, b) {
  return a + b;
}
// Calling function add1()
assert.equal(add1(5, 2), 7);
7.1.1.8 Pfeilfunktionsausdrücke

Pfeilfunktionsausdrücke werden insbesondere als Argumente von Funktionsaufrufen und Methodenaufrufen verwendet

const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);

// Equivalent to add2:
const add3 = (a, b) => a + b;

Der vorherige Code enthält die folgenden beiden Pfeilfunktionen (die Begriffe Ausdruck und Anweisung werden später in diesem Kapitel erklärt)

// An arrow function whose body is a code block
(a, b) => { return a + b }

// An arrow function whose body is an expression
(a, b) => a + b
7.1.1.9 Einfache Objekte
// Creating a plain object via an object literal
const obj = {
  first: 'Jane', // property
  last: 'Doe', // property
  getFullName() { // property (method)
    return this.first + ' ' + this.last;
  },
};

// Getting a property value
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';

// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');
7.1.1.10 Arrays
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);

// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';

// Adding an element to an Array:
arr.push('d');

assert.deepEqual(
  arr, ['a', 'β', 'c', 'd']);
7.1.1.11 Kontrollflussanweisungen

Bedingte Anweisung

if (x < 0) {
  x = -x;
}

for-of-Schleife

const arr = ['a', 'b'];
for (const element of arr) {
  console.log(element);
}
// Output:
// 'a'
// 'b'

7.1.2 Module

Jedes Modul ist eine einzelne Datei. Betrachten Sie zum Beispiel die folgenden beiden Dateien mit Modulen darin

file-tools.mjs
main.mjs

Das Modul in file-tools.mjs exportiert seine Funktion isTextFilePath()

export function isTextFilePath(filePath) {
  return filePath.endsWith('.txt');
}

Das Modul in main.mjs importiert das gesamte Modul path und die Funktion isTextFilePath()

// Import whole module as namespace object `path`
import * as path from 'path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';

7.1.3 Klassen

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `Person named ${this.name}`;
  }
  static logNames(persons) {
    for (const person of persons) {
      console.log(person.name);
    }
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)');

7.1.4 Fehlerbehandlung

function throwsException() {
  throw new Error('Problem!');
}

function catchesException() {
  try {
    throwsException();
  } catch (err) {
    assert.ok(err instanceof Error);
    assert.equal(err.message, 'Problem!');
  }
}

Hinweis

Die grammatikalische Kategorie von Variablennamen und Eigenschaftsnamen wird Bezeichner genannt.

Bezeichner dürfen folgende Zeichen enthalten

Einige Wörter haben in JavaScript eine besondere Bedeutung und werden als reserviert bezeichnet. Beispiele hierfür sind: if, true, const.

Reservierte Wörter dürfen nicht als Variablennamen verwendet werden

const if = 123;
  // SyntaxError: Unexpected token if

Sie sind jedoch als Eigenschaftsnamen zulässig

> const obj = { if: 123 };
> obj.if
123

7.1.6 Groß-/Kleinschreibung

Gängige Schreibweisen für die Verkettung von Wörtern sind

7.1.7 Namenskonventionen (Großschreibung)

Im Allgemeinen verwendet JavaScript Camel Case, mit Ausnahme von Konstanten.

Kleinbuchstaben

Großbuchstaben

7.1.8 Weitere Namenskonventionen

Die folgenden Namenskonventionen sind in JavaScript beliebt.

Wenn der Name eines Parameters mit einem Unterstrich beginnt (oder ein Unterstrich ist), bedeutet dies, dass dieser Parameter nicht verwendet wird – zum Beispiel

arr.map((_x, i) => i)

Wenn der Name einer Eigenschaft eines Objekts mit einem Unterstrich beginnt, gilt diese Eigenschaft als privat

class ValueWrapper {
  constructor(value) {
    this._value = value;
  }
}

7.1.9 Wohin mit den Semikolons?

Am Ende einer Anweisung

const x = 123;
func();

Aber nicht, wenn diese Anweisung mit einer geschweiften Klammer endet

while (false) {
  // ···
} // no semicolon

function func() {
  // ···
} // no semicolon

Das Hinzufügen eines Semikolons nach einer solchen Anweisung ist jedoch kein Syntaxfehler – es wird als leere Anweisung interpretiert

// Function declaration followed by empty statement:
function func() {
  // ···
};

  Quiz: Grundlagen

Siehe Quiz-App.

7.2 (Fortgeschritten)

Alle verbleibenden Abschnitte dieses Kapitels sind fortgeschritten.

7.3 Bezeichner

7.3.1 Gültige Bezeichner (Variablennamen usw.)

Erstes Zeichen

Nachfolgende Zeichen

Beispiele

const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;

7.3.2 Reservierte Wörter

Reservierte Wörter dürfen keine Variablennamen sein, aber sie dürfen Eigenschaftsnamen sein.

Alle JavaScript-Schlüsselwörter sind reservierte Wörter:

await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield

Die folgenden Tokens sind ebenfalls Schlüsselwörter, werden aber derzeit in der Sprache nicht verwendet

enum implements package protected interface private public

Die folgenden Literale sind reservierte Wörter

true false null

Technisch gesehen sind diese Wörter nicht reserviert, aber Sie sollten sie ebenfalls vermeiden, da sie praktisch Schlüsselwörter sind

Infinity NaN undefined async

Sie sollten auch nicht die Namen globaler Variablen (String, Math usw.) für Ihre eigenen Variablen und Parameter verwenden.

7.4 Anweisung vs. Ausdruck

In diesem Abschnitt untersuchen wir, wie JavaScript zwei Arten von syntaktischen Konstrukten unterscheidet: Anweisungen und Ausdrücke. Danach werden wir sehen, dass dies Probleme verursachen kann, da die gleiche Syntax unterschiedliche Bedeutungen haben kann, je nachdem, wo sie verwendet wird.

  Wir tun so, als gäbe es nur Anweisungen und Ausdrücke

Zur Vereinfachung tun wir so, als gäbe es in JavaScript nur Anweisungen und Ausdrücke.

7.4.1 Anweisungen

Eine Anweisung ist ein Code-Teil, der ausgeführt werden kann und eine Art Aktion ausführt. Zum Beispiel ist if eine Anweisung

let myStr;
if (myBool) {
  myStr = 'Yes';
} else {
  myStr = 'No';
}

Ein weiteres Beispiel für eine Anweisung: eine Funktionsdeklaration.

function twice(x) {
  return x + x;
}

7.4.2 Ausdrücke

Ein Ausdruck ist ein Code-Teil, der ausgewertet werden kann, um einen Wert zu erzeugen. Zum Beispiel ist der Code zwischen den Klammern ein Ausdruck

let myStr = (myBool ? 'Yes' : 'No');

Der Operator _?_:_, der zwischen den Klammern verwendet wird, wird als ternärer Operator bezeichnet. Es ist die Ausdrucksversion der if-Anweisung.

Sehen wir uns weitere Beispiele für Ausdrücke an. Wir geben Ausdrücke ein, und die REPL wertet sie für uns aus

> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true

7.4.3 Was ist wo erlaubt?

Die aktuelle Position im JavaScript-Quellcode bestimmt, welche Art von syntaktischen Konstrukten Sie verwenden dürfen

Ausdrücke können jedoch als Anweisungen verwendet werden. Dann werden sie Ausdrucksanweisungen genannt. Das Gegenteil ist nicht der Fall: Wenn der Kontext einen Ausdruck erfordert, können Sie keine Anweisung verwenden.

Der folgende Code zeigt, dass jeder Ausdruck bar() entweder Ausdruck oder Anweisung sein kann – es hängt vom Kontext ab

function f() {
  console.log(bar()); // bar() is expression
  bar(); // bar(); is (expression) statement  
}

7.5 Mehrdeutige Syntax

JavaScript hat mehrere Programmierkonstrukte, die syntaktisch mehrdeutig sind: Die gleiche Syntax wird unterschiedlich interpretiert, je nachdem, ob sie im Anweisungs- oder im Ausdruckskontext verwendet wird. Dieser Abschnitt untersucht das Phänomen und die Fallstricke, die es verursacht.

7.5.1 Gleiche Syntax: Funktionsdeklaration und Funktionsausdruck

Eine Funktionsdeklaration ist eine Anweisung

function id(x) {
  return x;
}

Ein Funktionsausdruck ist ein Ausdruck (rechte Seite von =)

const id = function me(x) {
  return x;
};

7.5.2 Gleiche Syntax: Objekt-Literal und Block

Im folgenden Code ist {} ein Objekt-Literal: ein Ausdruck, der ein leeres Objekt erstellt.

const obj = {};

Dies ist ein leerer Codeblock (eine Anweisung)

{
}

7.5.3 Eindeutigkeit schaffen

Die Mehrdeutigkeiten sind nur im Anweisungskontext ein Problem: Wenn der JavaScript-Parser auf mehrdeutige Syntax stößt, weiß er nicht, ob es sich um eine einfache Anweisung oder eine Ausdrucksanweisung handelt. Zum Beispiel

Um die Mehrdeutigkeit aufzulösen, werden Anweisungen, die mit function oder { beginnen, niemals als Ausdrücke interpretiert. Wenn Sie möchten, dass eine Ausdrucksanweisung mit einem dieser Tokens beginnt, müssen Sie sie in Klammern einschließen

(function (x) { console.log(x) })('abc');

// Output:
// 'abc'

In diesem Code

  1. Wir erstellen zunächst eine Funktion über einen Funktionsausdruck

    function (x) { console.log(x) }
  2. Dann rufen wir diese Funktion auf: ('abc')

Das in (1) gezeigte Codefragment wird nur als Ausdruck interpretiert, weil wir es in Klammern einschließen. Wenn wir es nicht täten, erhielten wir einen Syntaxfehler, da JavaScript dann eine Funktionsdeklaration erwartet und sich über den fehlenden Funktionsnamen beschwert. Außerdem können Sie keinen Funktionsaufruf direkt nach einer Funktionsdeklaration platzieren.

Später in diesem Buch werden wir weitere Beispiele für Fallstricke sehen, die durch syntaktische Mehrdeutigkeit verursacht werden

7.6 Semikolons

7.6.1 Faustregel für Semikolons

Jede Anweisung wird durch ein Semikolon beendet

const x = 3;
someFunction('abc');
i++;

außer Anweisungen, die mit Blöcken enden

function foo() {
  // ···
}
if (y > 0) {
  // ···
}

Der folgende Fall ist etwas knifflig

const func = () => {}; // semicolon!

Die gesamte const-Deklaration (eine Anweisung) endet mit einem Semikolon, aber darin befindet sich ein Pfeilfunktionsausdruck. Das heißt, nicht die Anweisung selbst endet mit einer geschweiften Klammer; es ist der eingebettete Pfeilfunktionsausdruck. Deshalb steht am Ende ein Semikolon.

7.6.2 Semikolons: Kontrollanweisungen

Der Körper einer Kontrollanweisung ist selbst eine Anweisung. Zum Beispiel ist dies die Syntax der while-Schleife

while (condition)
  statement

Der Körper kann eine einzelne Anweisung sein

while (a > 0) a--;

Aber Blöcke sind auch Anweisungen und daher zulässige Körper von Kontrollanweisungen

while (a > 0) {
  a--;
}

Wenn Sie möchten, dass eine Schleife einen leeren Körper hat, ist Ihre erste Option eine leere Anweisung (die nur ein Semikolon ist)

while (processNextItem() > 0);

Ihre zweite Option ist ein leerer Block

while (processNextItem() > 0) {}

7.7 Automatische Semikolon-Einfügung (ASI)

Obwohl ich empfehle, immer Semikolons zu schreiben, sind die meisten davon in JavaScript optional. Der Mechanismus, der dies ermöglicht, wird automatische Semikolon-Einfügung (ASI) genannt. In gewisser Weise korrigiert er Syntaxfehler.

ASI funktioniert wie folgt. Das Parsen einer Anweisung wird fortgesetzt, bis entweder Folgendes eintritt:

Mit anderen Worten, ASI kann als Einfügung von Semikolons an Zeilenumbrüchen betrachtet werden. Die nächsten Unterabschnitte behandeln die Fallstricke von ASI.

7.7.1 ASI unerwartet ausgelöst

Die gute Nachricht über ASI ist, dass es – wenn Sie sich nicht darauf verlassen und immer Semikolons schreiben – nur einen Fallstrick gibt, den Sie beachten müssen. Es ist, dass JavaScript Zeilenumbrüche nach bestimmten Tokens verbietet. Wenn Sie einen Zeilenumbruch einfügen, wird auch ein Semikolon eingefügt.

Das Token, bei dem dies am praktischsten relevant ist, ist return. Betrachten Sie zum Beispiel den folgenden Code

return
{
  first: 'jane'
};

Dieser Code wird geparst als

return;
{
  first: 'jane';
}
;

Das heißt

Warum macht JavaScript das? Es schützt davor, versehentlich einen Wert in einer Zeile nach einem return zurückzugeben.

7.7.2 ASI unerwartet nicht ausgelöst

In einigen Fällen wird ASI nicht ausgelöst, wenn Sie denken, dass es ausgelöst werden sollte. Das macht das Leben komplizierter für Leute, die Semikolons nicht mögen, weil sie sich dieser Fälle bewusst sein müssen. Die folgenden drei Beispiele sind dafür gedacht. Es gibt mehr.

Beispiel 1: Unbeabsichtigter Funktionsaufruf.

a = b + c
(d + e).print()

Geparst als

a = b + c(d + e).print();

Beispiel 2: Unbeabsichtigte Division.

a = b
/hi/g.exec(c).map(d)

Geparst als

a = b / hi / g.exec(c).map(d);

Beispiel 3: Unbeabsichtigter Eigenschaftszugriff.

someFunction()
['ul', 'ol'].map(x => x + x)

Ausgeführt als

const propKey = ('ul','ol'); // comma operator
assert.equal(propKey, 'ol');

someFunction()[propKey].map(x => x + x);

7.8 Semikolons: Best Practices

Ich empfehle, immer Semikolons zu schreiben

Es gibt jedoch auch viele Leute, die die zusätzliche visuelle Unordnung von Semikolons nicht mögen. Wenn Sie einer von ihnen sind: Code ohne sie ist legal. Ich empfehle, Werkzeuge zu verwenden, die Ihnen helfen, Fehler zu vermeiden. Die folgenden zwei Beispiele sind dafür gedacht

7.9 Strict Mode vs. Sloppy Mode

Ab ECMAScript 5 hat JavaScript zwei Modi, in denen JavaScript ausgeführt werden kann

Sie werden Sloppy Mode in modernem JavaScript-Code selten antreffen, der fast immer in Modulen steht. In diesem Buch gehe ich davon aus, dass Strict Mode immer eingeschaltet ist.

7.9.1 Strict Mode einschalten

In Skriptdateien und CommonJS-Modulen schalten Sie Strict Mode für eine vollständige Datei ein, indem Sie den folgenden Code in die erste Zeile setzen

'use strict';

Das Tolle an dieser „Direktive“ ist, dass ECMAScript-Versionen vor 5 sie einfach ignorieren: Sie ist eine Ausdrucksanweisung, die nichts tut.

Sie können Strict Mode auch nur für eine einzelne Funktion einschalten

function functionInStrictMode() {
  'use strict';
}

7.9.2 Verbesserungen im Strict Mode

Betrachten wir drei Dinge, die Strict Mode besser macht als Sloppy Mode. Alle Codefragmente in diesem Abschnitt werden im Sloppy Mode ausgeführt.

7.9.2.1 Fallstrick im Sloppy Mode: Das Ändern einer nicht deklarierten Variablen erstellt eine globale Variable

Im Nicht-Strict-Modus erstellt das Ändern einer nicht deklarierten Variablen eine globale Variable.

function sloppyFunc() {
  undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);

Strict Mode macht es besser und löst einen ReferenceError aus. Das erleichtert die Erkennung von Tippfehlern.

function strictFunc() {
  'use strict';
  undeclaredVar2 = 123;
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'undeclaredVar2 is not defined',
  });

assert.throws() besagt, dass sein erstes Argument, eine Funktion, einen ReferenceError auslöst, wenn sie aufgerufen wird.

Funktionsdeklarationen sind im Strict Mode blockbezogen, im Sloppy Mode funktionsbezogen

Im Strict Mode existiert eine Variable, die über eine Funktionsdeklaration erstellt wurde, nur innerhalb des innersten umschließenden Blocks

function strictFunc() {
  'use strict';
  {
    function foo() { return 123 }
  }
  return foo(); // ReferenceError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'foo is not defined',
  });

Im Sloppy Mode sind Funktionsdeklarationen funktionsbezogen

function sloppyFunc() {
  {
    function foo() { return 123 }
  }
  return foo(); // works
}
assert.equal(sloppyFunc(), 123);
7.9.2.3 Sloppy Mode löst keine Ausnahmen aus, wenn unveränderliche Daten geändert werden

Im Strict Mode erhalten Sie eine Ausnahme, wenn Sie versuchen, unveränderliche Daten zu ändern

function strictFunc() {
  'use strict';
  true.prop = 1; // TypeError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'TypeError',
    message: "Cannot create property 'prop' on boolean 'true'",
  });

Im Sloppy Mode schlägt die Zuweisung stillschweigend fehl

function sloppyFunc() {
  true.prop = 1; // fails silently
  return true.prop;
}
assert.equal(sloppyFunc(), undefined);

  Weitere Lektüre: Sloppy Mode

Weitere Informationen darüber, wie sich Sloppy Mode von Strict Mode unterscheidet, finden Sie unter MDN.

  Quiz: Fortgeschritten

Siehe Quiz-App.