9. Variablen und Scoping
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

9. Variablen und Scoping



9.1 Überblick

ES6 bietet zwei neue Möglichkeiten zur Deklaration von Variablen: let und const, die die ES5-Methode zur Variablendeklaration, var, größtenteils ersetzen.

9.1.1 let

let funktioniert ähnlich wie var, aber die Variable, die es deklariert, ist *block-bezogen*, sie existiert nur innerhalb des aktuellen Blocks. var ist *funktionsbezogen*.

Im folgenden Code sehen Sie, dass die mit let deklarierte Variable tmp nur innerhalb des Blocks existiert, der in Zeile A beginnt

function order(x, y) {
    if (x > y) { // (A)
        let tmp = x;
        x = y;
        y = tmp;
    }
    console.log(tmp===x); // ReferenceError: tmp is not defined
    return [x, y];
}

9.1.2 const

const funktioniert wie let, aber die Variable, die Sie deklarieren, muss sofort initialisiert werden, mit einem Wert, der danach nicht mehr geändert werden kann.

const foo;
    // SyntaxError: missing = in const declaration

const bar = 123;
bar = 456;
    // TypeError: `bar` is read-only

Da for-of pro Schleifeniteration eine *Bindung* (Speicherplatz für eine Variable) erstellt, ist es in Ordnung, die Schleifenvariable mit const zu deklarieren

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

9.1.3 Möglichkeiten der Variablendeklaration

Die folgende Tabelle gibt einen Überblick über sechs Möglichkeiten, wie Variablen in ES6 deklariert werden können (inspiriert von einer Tabelle von kangax)

  Hoisting Geltungsbereich Erzeugt globale Eigenschaften
var Deklaration Funktion Ja
let Temporäre Sackgasse Block Nein
const Temporäre Sackgasse Block Nein
Funktion Vollständig Block Ja
class Nein Block Nein
import Vollständig Modul-global Nein

9.2 Block-Scoping durch let und const

Sowohl let als auch const erstellen Variablen, die *block-bezogen* sind – sie existieren nur innerhalb des innersten Blocks, der sie umschließt. Der folgende Code zeigt, dass die mit const deklarierte Variable tmp nur innerhalb des Blocks der if-Anweisung existiert

function func() {
    if (true) {
        const tmp = 123;
    }
    console.log(tmp); // ReferenceError: tmp is not defined
}

Im Gegensatz dazu sind var-deklarierte Variablen funktionsbezogen

function func() {
    if (true) {
        var tmp = 123;
    }
    console.log(tmp); // 123
}

Block-Scoping bedeutet, dass Sie Variablen innerhalb einer Funktion überschatten können

function func() {
  const foo = 5;
  if (···) {
     const foo = 10; // shadows outer `foo`
     console.log(foo); // 10
  }
  console.log(foo); // 5
}

9.3 const erstellt unveränderliche Variablen

Variablen, die mit let erstellt wurden, sind veränderlich

let foo = 'abc';
foo = 'def';
console.log(foo); // def

Konstanten, Variablen, die mit const erstellt wurden, sind unveränderlich – Sie können ihnen keine anderen Werte zuweisen

const foo = 'abc';
foo = 'def'; // TypeError

9.3.1 Fallstrick: const macht den Wert nicht unveränderlich

const bedeutet nur, dass eine Variable immer denselben Wert hat, aber es bedeutet nicht, dass der Wert selbst unveränderlich ist oder wird. Zum Beispiel ist obj eine Konstante, aber der Wert, auf den sie zeigt, ist veränderlich – wir können ihr eine Eigenschaft hinzufügen

const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123

Wir können obj jedoch keinen anderen Wert zuweisen

obj = {}; // TypeError

Wenn Sie möchten, dass der Wert von obj unveränderlich ist, müssen Sie sich selbst darum kümmern. Zum Beispiel, indem Sie ihn einfrieren

const obj = Object.freeze({});
obj.prop = 123; // TypeError
9.3.1.1 Fallstrick: Object.freeze() ist flach

Beachten Sie, dass Object.freeze() *flach* ist, es friert nur die Eigenschaften seines Arguments ein, nicht die Objekte, die in seinen Eigenschaften gespeichert sind. Zum Beispiel ist das Objekt obj eingefroren

> const obj = Object.freeze({ foo: {} });
> obj.bar = 123
TypeError: Can't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property 'foo' of #<Object>

Aber das Objekt obj.foo nicht.

> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'

9.3.2 const in Schleifenkörpern

Sobald eine const-Variable erstellt wurde, kann sie nicht mehr geändert werden. Das bedeutet aber nicht, dass Sie ihren Geltungsbereich nicht wieder betreten und neu starten können, mit einem neuen Wert. Zum Beispiel über eine Schleife

function logArgs(...args) {
    for (const [index, elem] of args.entries()) { // (A)
        const message = index + '. ' + elem; // (B)
        console.log(message);
    }
}
logArgs('Hello', 'everyone');

// Output:
// 0. Hello
// 1. everyone

In diesem Code gibt es zwei const-Deklarationen, in Zeile A und in Zeile B. Und während jeder Schleifeniteration haben ihre Konstanten unterschiedliche Werte.

9.4 Die temporäre Sackgasse (Temporal Dead Zone)

Eine Variable, die mit let oder const deklariert wurde, hat eine sogenannte *temporäre Sackgasse* (TDZ): Wenn ihr Geltungsbereich betreten wird, kann sie nicht aufgerufen werden (lesen oder schreiben), bis die Ausführung die Deklaration erreicht. Vergleichen wir die Lebenszyklen von var-deklarierten Variablen (die keine TDZs haben) und let-deklarierten Variablen (die TDZs haben).

9.4.1 Der Lebenszyklus von var-deklarierten Variablen

var-Variablen haben keine temporären Sackgassen. Ihr Lebenszyklus umfasst die folgenden Schritte

9.4.2 Der Lebenszyklus von let-deklarierten Variablen

Variablen, die über let deklariert wurden, haben temporäre Sackgassen und ihr Lebenszyklus sieht wie folgt aus

const-Variablen funktionieren ähnlich wie let-Variablen, müssen aber einen Initialisierer haben (d.h. sofort einen Wert zugewiesen bekommen) und können nicht geändert werden.

9.4.3 Beispiele

Innerhalb einer TDZ wird eine Ausnahme ausgelöst, wenn eine Variable gelesen oder geschrieben wird

let tmp = true;
if (true) { // enter new scope, TDZ starts
    // Uninitialized binding for `tmp` is created
    console.log(tmp); // ReferenceError

    let tmp; // TDZ ends, `tmp` is initialized with `undefined`
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
}
console.log(tmp); // true

Wenn ein Initialisierer vorhanden ist, endet die TDZ *nachdem* der Initialisierer ausgewertet und das Ergebnis der Variable zugewiesen wurde

let foo = console.log(foo); // ReferenceError

Der folgende Code zeigt, dass die Sackgasse wirklich *temporär* (zeitlich bedingt) und nicht räumlich (örtlich bedingt) ist

if (true) { // enter new scope, TDZ starts
    const func = function () {
        console.log(myVar); // OK!
    };

    // Here we are within the TDZ and
    // accessing `myVar` would cause a `ReferenceError`

    let myVar = 3; // TDZ ends
    func(); // called outside TDZ
}

9.4.4 typeof wirft einen ReferenceError für eine Variable in der TDZ

Wenn Sie über typeof auf eine Variable in der temporären Sackgasse zugreifen, erhalten Sie eine Ausnahme

if (true) {
    console.log(typeof foo); // ReferenceError (TDZ)
    console.log(typeof aVariableThatDoesntExist); // 'undefined'
    let foo;
}

Warum? Die Begründung lautet wie folgt: foo ist nicht unde deklariert, sondern uninitialisiert. Sie sollten sich seiner Existenz bewusst sein, sind es aber nicht. Daher ist es wünschenswert, gewarnt zu werden.

Darüber hinaus ist diese Art der Überprüfung nur nützlich, um globale Variablen bedingt zu erstellen. Das ist etwas, das Sie in normalen Programmen nicht tun müssen.

9.4.4.1 Bedingtes Erstellen von Variablen

Wenn es um das bedingte Erstellen von Variablen geht, haben Sie zwei Möglichkeiten.

Option 1 – typeof und var

if (typeof someGlobal === 'undefined') {
    var someGlobal = { ··· };
}

Diese Option funktioniert nur im globalen Geltungsbereich (und daher nicht innerhalb von ES6-Modulen).

Option 2 – window

if (!('someGlobal' in window)) {
    window.someGlobal = { ··· };
}

9.4.5 Warum gibt es eine temporäre Sackgasse?

Es gibt mehrere Gründe, warum const und let temporäre Sackgassen haben

9.4.6 Weiterführende Lektüre

Quellen dieses Abschnitts

9.5 let und const in Schleifenkopfzeilen

Die folgenden Schleifen erlauben Ihnen, Variablen in ihren Kopfzeilen zu deklarieren

Um eine Deklaration vorzunehmen, können Sie entweder var, let oder const verwenden. Jede davon hat eine andere Wirkung, wie ich als Nächstes erklären werde.

9.5.1 for-Schleife

Das Deklarieren einer Variable mit var im Kopf einer for-Schleife erstellt eine einzelne *Bindung* (Speicherplatz) für diese Variable

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]

Jedes i in den Körpern der drei Pfeilfunktionen bezieht sich auf dieselbe Bindung, weshalb sie alle denselben Wert zurückgeben.

Wenn Sie eine Variable mit let deklarieren, wird für jede Schleifeniteration eine neue Bindung erstellt

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]

Diesmal bezieht sich jedes i auf die Bindung einer bestimmten Iteration und bewahrt den zu diesem Zeitpunkt aktuellen Wert. Daher gibt jede Pfeilfunktion einen anderen Wert zurück.

const funktioniert wie var, aber Sie können den Anfangswert einer mit const deklarierten Variable nicht ändern

// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
    console.log(i);
}

Das Erhalten einer neuen Bindung für jede Iteration mag zunächst seltsam erscheinen, ist aber sehr nützlich, wenn Sie Schleifen verwenden, um Funktionen zu erstellen, die sich auf Schleifenvariablen beziehen, wie in einem späteren Abschnitt erklärt.

9.5.2 for-of-Schleife und for-in-Schleife

In einer for-of-Schleife erstellt var eine einzelne Bindung

const arr = [];
for (var i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [2,2,2]

const erstellt eine unveränderliche Bindung pro Iteration

const arr = [];
for (const i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]

let erstellt ebenfalls eine Bindung pro Iteration, aber die von ihm erstellten Bindungen sind veränderlich.

Die for-in-Schleife funktioniert ähnlich wie die for-of-Schleife.

9.5.3 Warum sind Iterationsbindungen nützlich?

Das Folgende ist eine HTML-Seite, die drei Links anzeigt

  1. Wenn Sie auf „ja“ klicken, wird es mit „ja“ übersetzt.
  2. Wenn Sie auf „nein“ klicken, wird es mit „nein“ übersetzt.
  3. Wenn Sie auf „vielleicht“ klicken, wird es mit „vielleicht“ übersetzt.
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="content"></div>
    <script>
        const entries = [
            ['yes', 'ja'],
            ['no', 'nein'],
            ['perhaps', 'vielleicht'],
        ];
        const content = document.getElementById('content');
        for (const [source, target] of entries) { // (A)
            content.insertAdjacentHTML('beforeend',
                `<div><a id="${source}" href="">${source}</a></div>`);
            document.getElementById(source).addEventListener(
                'click', (event) => {
                    event.preventDefault();
                    alert(target); // (B)
                });
        }
    </script>
</body>
</html>

Was angezeigt wird, hängt von der Variable target (Zeile B) ab. Wenn wir statt const in Zeile A var verwendet hätten, gäbe es eine einzige Bindung für die gesamte Schleife und target hätte danach den Wert 'vielleicht'. Daher würden Sie, egal auf welchen Link Sie klicken, immer die Übersetzung 'vielleicht' erhalten.

Glücklicherweise erhalten wir mit const eine Bindung pro Schleifeniteration und die Übersetzungen werden korrekt angezeigt.

9.6 Parameter als Variablen

9.6.1 Parameter versus lokale Variablen

Wenn Sie eine Variable mit let deklarieren, die denselben Namen wie ein Parameter hat, erhalten Sie einen statischen Fehler (zur Ladezeit)

function func(arg) {
    let arg; // static error: duplicate declaration of `arg`
}

Das Gleiche innerhalb eines Blocks überschattet den Parameter

function func(arg) {
    {
        let arg; // shadows parameter `arg`
    }
}

Im Gegensatz dazu bewirkt das Deklarieren einer Variable mit var, die denselben Namen wie ein Parameter hat, nichts, genau wie das erneute Deklarieren einer var-Variable im selben Geltungsbereich nichts bewirkt.

function func(arg) {
    var arg; // does nothing
}
function func(arg) {
    {
        // We are still in same `var` scope as `arg`
        var arg; // does nothing
    }
}

9.6.2 Parameter-Standardwerte und die temporäre Sackgasse

Wenn Parameter Standardwerte haben, werden sie wie eine Sequenz von let-Anweisungen behandelt und unterliegen temporären Sackgassen

// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// Exception: `x` tries to access `y` within TDZ
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError

9.6.3 Parameter-Standardwerte sehen den Geltungsbereich des Körpers nicht

Der Geltungsbereich von Parameter-Standardwerten ist getrennt vom Geltungsbereich des Körpers (der erstere umschließt letzteren). Das bedeutet, dass Methoden oder Funktionen, die „innerhalb“ von Parameter-Standardwerten definiert sind, die lokalen Variablen des Körpers nicht sehen

const foo = 'outer';
function bar(func = x => foo) {
    const foo = 'inner';
    console.log(func()); // outer
}
bar();

9.7 Das globale Objekt

Das globale Objekt von JavaScript (window in Webbrowsern, global in Node.js) ist eher ein Fehler als eine Funktion, insbesondere in Bezug auf die Leistung. Deshalb ist es sinnvoll, dass ES6 eine Unterscheidung einführt

Beachten Sie, dass die Körper von Modulen nicht im globalen Geltungsbereich ausgeführt werden, sondern nur Skripte. Daher bilden die Umgebungen für verschiedene Variablen die folgende Kette.

9.8 Funktionsdeklarationen und Klassendeklarationen

Funktionsdeklarationen...

Der folgende Code zeigt das Hoisting von Funktionsdeklarationen

{ // Enter a new scope

    console.log(foo()); // OK, due to hoisting
    function foo() {
        return 'hello';
    }
}

Klassendeklarationen...

Dass Klassen nicht gehoisted werden, mag überraschend sein, denn unter der Haube erstellen sie Funktionen. Die Begründung für dieses Verhalten ist, dass die Werte ihrer extends-Klauseln über Ausdrücke definiert werden und diese Ausdrücke zur richtigen Zeit ausgeführt werden müssen.

{ // Enter a new scope

    const identity = x => x;

    // Here we are in the temporal dead zone of `MyClass`
    const inst = new MyClass(); // ReferenceError

    // Note the expression in the `extends` clause
    class MyClass extends identity(Object) {
    }
}

9.9 Codierungsstil: const versus let versus var

Ich empfehle, immer entweder let oder const zu verwenden

  1. Bevorzugen Sie const. Sie können es verwenden, wenn eine Variable ihren Wert nie ändert. Mit anderen Worten: Die Variable sollte nie die linke Seite einer Zuweisung oder Operand von ++ oder -- sein. Das Ändern eines Objekts, auf das eine const-Variable verweist, ist erlaubt
     const foo = {};
     foo.prop = 123; // OK
    

    Sie können const sogar in einer for-of-Schleife verwenden, da pro Schleifeniteration eine (unveränderliche) Bindung erstellt wird

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

    Innerhalb des Körpers der for-of-Schleife kann x nicht geändert werden.

  2. Andernfalls verwenden Sie let – wenn sich der Anfangswert einer Variable später ändert.
     let counter = 0; // initial value
     counter++; // change
    
     let obj = {}; // initial value
     obj = { foo: 123 }; // change
    
  3. Vermeiden Sie var.

Wenn Sie diese Regeln befolgen, wird var nur in Legacy-Code erscheinen, als Signal, dass sorgfältiges Refactoring erforderlich ist.

var tut eine Sache, die let und const nicht tun: Variablen, die damit deklariert werden, werden zu Eigenschaften des globalen Objekts. Das ist im Allgemeinen keine gute Sache. Sie können den gleichen Effekt erzielen, indem Sie window (in Browsern) oder global (in Node.js) zuweisen.

9.9.1 Ein alternativer Ansatz

Eine Alternative zu den gerade erwähnten Stilregeln ist, const nur für Dinge zu verwenden, die vollständig unveränderlich sind (Primitive Werte und eingefrorene Objekte). Dann haben wir zwei Ansätze

  1. Bevorzugen Sie const: const kennzeichnet unveränderliche Bindungen.
  2. Bevorzugen Sie let: const kennzeichnet unveränderliche Werte.

Ich neige leicht zu #1, aber #2 ist auch in Ordnung.

Weiter: 10. Destrukturierung