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

11 Variablen und Zuweisung



Dies sind die wichtigsten Arten, wie JavaScript Variablen deklariert

Vor ES6 gab es auch var. Aber es hat mehrere Eigenheiten, daher ist es am besten, es in modernem JavaScript zu vermeiden. Sie können mehr darüber in Speaking JavaScript lesen.

11.1 let

Über let deklarierte Variablen sind veränderlich

let i;
i = 0;
i = i + 1;
assert.equal(i, 1);

Sie können auch gleichzeitig deklarieren und zuweisen

let i = 0;

11.2 const

Über const deklarierte Variablen sind unveränderlich. Sie müssen immer sofort initialisiert werden

const i = 0; // must initialize

assert.throws(
  () => { i = i + 1 },
  {
    name: 'TypeError',
    message: 'Assignment to constant variable.',
  }
);

11.2.1 const und Unveränderlichkeit

In JavaScript bedeutet const nur, dass die Bindung (die Verknüpfung zwischen Variablenname und Variablenwert) unveränderlich ist. Der Wert selbst kann veränderlich sein, wie obj im folgenden Beispiel.

const obj = { prop: 0 };

// Allowed: changing properties of `obj`
obj.prop = obj.prop + 1;
assert.equal(obj.prop, 1);

// Not allowed: assigning to `obj`
assert.throws(
  () => { obj = {} },
  {
    name: 'TypeError',
    message: 'Assignment to constant variable.',
  }
);

11.2.2 const und Schleifen

Sie können const mit for-of-Schleifen verwenden, wobei für jede Iteration eine neue Bindung erstellt wird

const arr = ['hello', 'world'];
for (const elem of arr) {
  console.log(elem);
}
// Output:
// 'hello'
// 'world'

In einfachen for-Schleifen müssen Sie jedoch let verwenden

const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
  const elem = arr[i];
  console.log(elem);
}

11.3 Entscheidung zwischen const und let

Ich empfehle die folgenden Regeln, um zwischen const und let zu entscheiden

  Übung: const

exercises/variables-assignment/const_exrc.mjs

11.4 Der Geltungsbereich (Scope) einer Variablen

Der Geltungsbereich einer Variablen ist der Bereich eines Programms, in dem sie zugänglich ist. Betrachten Sie den folgenden Code.

{ // // Scope A. Accessible: x
  const x = 0;
  assert.equal(x, 0);
  { // Scope B. Accessible: x, y
    const y = 1;
    assert.equal(x, 0);
    assert.equal(y, 1);
    { // Scope C. Accessible: x, y, z
      const z = 2;
      assert.equal(x, 0);
      assert.equal(y, 1);
      assert.equal(z, 2);
    }
  }
}
// Outside. Not accessible: x, y, z
assert.throws(
  () => console.log(x),
  {
    name: 'ReferenceError',
    message: 'x is not defined',
  }
);

Jede Variable ist in ihrem direkten Geltungsbereich und allen darin verschachtelten Bereichen zugänglich.

Die über const und let deklarierten Variablen werden als Block-Scope bezeichnet, da ihre Geltungsbereiche immer die innersten umgebenden Blöcke sind.

11.4.1 Überschattende Variablen (Shadowing)

Sie können nicht dieselbe Variable zweimal auf derselben Ebene deklarieren

assert.throws(
  () => {
    eval('let x = 1; let x = 2;');
  },
  {
    name: 'SyntaxError',
    message: "Identifier 'x' has already been declared",
  });

  Warum eval()?

eval() verzögert die Analyse (und damit den SyntaxError) bis zur Ausführung des Rückrufs von assert.throws(). Wenn wir es nicht verwenden würden, erhielten wir bereits einen Fehler, wenn dieser Code analysiert wird, und assert.throws() würde nicht einmal ausgeführt werden.

Sie können jedoch einen Block verschachteln und denselben Variablennamen x verwenden, den Sie außerhalb des Blocks verwendet haben

const x = 1;
assert.equal(x, 1);
{
  const x = 2;
  assert.equal(x, 2);
}
assert.equal(x, 1);

Innerhalb des Blocks ist das innere x die einzige zugängliche Variable mit diesem Namen. Das innere x soll das äußere x überschatten. Sobald Sie den Block verlassen, können Sie wieder auf den alten Wert zugreifen.

  Quiz: Grundlagen

Siehe Quiz-App.

11.5 (Fortgeschritten)

Alle verbleibenden Abschnitte sind fortgeschritten.

11.6 Terminologie: statisch vs. dynamisch

Diese beiden Adjektive beschreiben Phänomene in Programmiersprachen

Betrachten wir Beispiele für diese beiden Begriffe.

11.6.1 Statisches Phänomen: Geltungsbereiche von Variablen

Variablen-Geltungsbereiche sind ein statisches Phänomen. Betrachten Sie den folgenden Code

function f() {
  const x = 3;
  // ···
}

x ist statisch (oder lexikalisch) eingeschränkt. Das heißt, sein Geltungsbereich ist fest und ändert sich zur Laufzeit nicht.

Variablen-Geltungsbereiche bilden einen statischen Baum (durch statische Verschachtelung).

11.6.2 Dynamisches Phänomen: Funktionsaufrufe

Funktionsaufrufe sind ein dynamisches Phänomen. Betrachten Sie den folgenden Code

function g(x) {}
function h(y) {
  if (Math.random()) g(y); // (A)
}

Ob der Funktionsaufruf in Zeile A stattfindet, kann erst zur Laufzeit entschieden werden.

Funktionsaufrufe bilden einen dynamischen Baum (durch dynamische Aufrufe).

11.7 Globale Variablen und das globale Objekt

Die Variablen-Geltungsbereiche von JavaScript sind verschachtelt. Sie bilden einen Baum

Die Wurzel wird auch als globaler Geltungsbereich bezeichnet. In Webbrowsern ist der einzige Ort, an dem man sich direkt in diesem Bereich befindet, auf der obersten Ebene eines Skripts. Die Variablen des globalen Geltungsbereichs werden als globale Variablen bezeichnet und sind überall zugänglich. Es gibt zwei Arten von globalen Variablen

Das folgende HTML-Fragment demonstriert globalThis und die beiden Arten von globalen Variablen.

<script>
  const declarativeVariable = 'd';
  var objectVariable = 'o';
</script>
<script>
  // All scripts share the same top-level scope:
  console.log(declarativeVariable); // 'd'
  console.log(objectVariable); // 'o'
  
  // Not all declarations create properties of the global object:
  console.log(globalThis.declarativeVariable); // undefined
  console.log(globalThis.objectVariable); // 'o'
</script>

Jedes ECMAScript-Modul hat seinen eigenen Geltungsbereich. Daher sind Variablen, die auf der obersten Ebene eines Moduls existieren, nicht global. Abb. 5 veranschaulicht, wie die verschiedenen Geltungsbereiche miteinander verbunden sind.

Figure 5: The global scope is JavaScript’s outermost scope. It has two kinds of variables: object variables (managed via the global object) and normal declarative variables. Each ECMAScript module has its own scope which is contained in the global scope.

11.7.1 globalThis [ES2020]

Die globale Variable globalThis ist die neue Standardmethode für den Zugriff auf das globale Objekt. Sie hat ihren Namen von der Tatsache, dass sie denselben Wert wie this im globalen Geltungsbereich hat.

  globalThis zeigt nicht immer direkt auf das globale Objekt

Zum Beispiel gibt es in Browsern eine Indirektion. Diese Indirektion ist normalerweise nicht bemerkbar, aber sie existiert und kann beobachtet werden.

11.7.1.1 Alternativen zu globalThis

Ältere Methoden für den Zugriff auf das globale Objekt hängen von der Plattform ab

11.7.1.2 Anwendungsfälle für globalThis

Das globale Objekt wird jetzt als ein Fehler betrachtet, von dem sich JavaScript aufgrund von Abwärtskompatibilität nicht trennen kann. Es beeinträchtigt die Leistung negativ und ist im Allgemeinen verwirrend.

ECMAScript 6 führte mehrere Funktionen ein, die es einfacher machen, das globale Objekt zu vermeiden – zum Beispiel

Es ist normalerweise besser, auf globale Objektvariablen über Variablen und nicht über Eigenschaften von globalThis zuzugreifen. Ersteres hat auf allen JavaScript-Plattformen immer gleich funktioniert.

Tutorials im Web greifen gelegentlich auf globale Variablen globVar über window.globVar zu. Aber das Präfix „window.“ ist nicht notwendig, und ich empfehle, es wegzulassen

window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes

Daher gibt es relativ wenige Anwendungsfälle für globalThis – zum Beispiel

11.8 Deklarationen: Geltungsbereich und Aktivierung

Dies sind zwei Schlüsselaspekte von Deklarationen

Tabelle 1 fasst zusammen, wie verschiedene Deklarationen mit diesen Aspekten umgehen.

Tabelle 1: Aspekte von Deklarationen. „Duplikate“ beschreibt, ob eine Deklaration mit demselben Namen (pro Geltungsbereich) zweimal verwendet werden kann. „Global prop.“ beschreibt, ob eine Deklaration eine Eigenschaft des globalen Objekts hinzufügt, wenn sie im globalen Geltungsbereich eines Skripts ausgeführt wird. TDZ bedeutet temporäre tote Zone (die später erklärt wird). (*) Funktionsdeklarationen haben normalerweise Block-Scope, aber Funktions-Scope in lässigem Modus (sloppy mode).
Geltungsbereich Aktivierung Duplikate Global prop.
const Block Dekl. (TDZ)
let Block Dekl. (TDZ)
Funktion Block (*) Start
class Block Dekl. (TDZ)
import Modul gleich wie export
var Funktion Start, teilweise

import wird in §27.5 „ECMAScript-Module“ beschrieben. Die folgenden Abschnitte beschreiben die anderen Konstrukte detaillierter.

11.8.1 const und let: temporäre tote Zone (temporal dead zone)

Für JavaScript musste TC39 entscheiden, was passiert, wenn man auf eine Konstante in ihrem direkten Geltungsbereich zugreift, bevor sie deklariert wurde

{
  console.log(x); // What happens here?
  const x;
}

Einige mögliche Ansätze sind

  1. Der Name wird im umgebenden Geltungsbereich aufgelöst.
  2. Sie erhalten undefined.
  3. Es gibt einen Fehler.

Ansatz 1 wurde abgelehnt, da es keinen Präzedenzfall in der Sprache für diesen Ansatz gibt. Er wäre für JavaScript-Programmierer nicht intuitiv.

Ansatz 2 wurde abgelehnt, da x dann keine Konstante wäre – sie hätte unterschiedliche Werte vor und nach ihrer Deklaration.

let verwendet denselben Ansatz 3 wie const, so dass beide ähnlich funktionieren und es einfach ist, zwischen ihnen zu wechseln.

Die Zeit zwischen dem Betreten des Geltungsbereichs einer Variablen und der Ausführung ihrer Deklaration wird als temporäre tote Zone (TDZ) dieser Variablen bezeichnet

Der folgende Code veranschaulicht die temporäre tote Zone

if (true) { // entering scope of `tmp`, TDZ starts
  // `tmp` is uninitialized:
  assert.throws(() => (tmp = 'abc'), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp; // TDZ ends
  assert.equal(tmp, undefined);
}

Das nächste Beispiel zeigt, dass die temporäre tote Zone wirklich temporal (zeitbezogen) ist

if (true) { // entering scope of `myVar`, TDZ starts
  const func = () => {
    console.log(myVar); // executed later
  };

  // We are within the TDZ:
  // Accessing `myVar` causes `ReferenceError`

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

Obwohl func() vor der Deklaration von myVar liegt und diese Variable verwendet, können wir func() aufrufen. Aber wir müssen warten, bis die temporäre tote Zone von myVar vorbei ist.

11.8.2 Funktionsdeklarationen und frühe Aktivierung

  Mehr Informationen zu Funktionen

In diesem Abschnitt verwenden wir Funktionen – bevor wir die Gelegenheit hatten, sie richtig zu lernen. Hoffentlich ergibt alles Sinn. Wann immer dies nicht der Fall ist, siehe §25 „Callable Values“.

Eine Funktionsdeklaration wird immer beim Betreten ihres Geltungsbereichs ausgeführt, unabhängig davon, wo sie sich innerhalb dieses Geltungsbereichs befindet. Das ermöglicht es Ihnen, eine Funktion foo() aufzurufen, bevor sie deklariert wird

assert.equal(foo(), 123); // OK
function foo() { return 123; }

Die frühe Aktivierung von foo() bedeutet, dass der vorherige Code äquivalent ist zu

function foo() { return 123; }
assert.equal(foo(), 123);

Wenn Sie eine Funktion über const oder let deklarieren, wird sie nicht frühzeitig aktiviert. Im folgenden Beispiel können Sie bar() erst nach ihrer Deklaration verwenden.

assert.throws(
  () => bar(), // before declaration
  ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // after declaration 
11.8.2.1 Vorausruf ohne frühe Aktivierung

Auch wenn eine Funktion g() nicht frühzeitig aktiviert wird, kann sie von einer vorhergehenden Funktion f() (im selben Geltungsbereich) aufgerufen werden, wenn wir die folgende Regel beachten: f() muss nach der Deklaration von g() aufgerufen werden.

const f = () => g();
const g = () => 123;

// We call f() after g() was declared:
assert.equal(f(), 123);

Die Funktionen eines Moduls werden normalerweise aufgerufen, nachdem sein vollständiger Körper ausgeführt wurde. Daher müssen Sie sich in Modulen selten um die Reihenfolge der Funktionen kümmern.

Zuletzt beachten Sie, wie die frühe Aktivierung automatisch die oben genannte Regel beibehält: Beim Betreten eines Geltungsbereichs werden alle Funktionsdeklarationen zuerst ausgeführt, bevor irgendwelche Aufrufe getätigt werden.

11.8.2.2 Eine Fallstrick der frühen Aktivierung

Wenn Sie sich auf die frühe Aktivierung verlassen, um eine Funktion vor ihrer Deklaration aufzurufen, müssen Sie vorsichtig sein, dass sie nicht auf Daten zugreift, die nicht frühzeitig aktiviert werden.

funcDecl();

const MY_STR = 'abc';
function funcDecl() {
  assert.throws(
    () => MY_STR,
    ReferenceError);
}

Das Problem verschwindet, wenn Sie den Aufruf von funcDecl() nach der Deklaration von MY_STR tätigen.

11.8.2.3 Die Vor- und Nachteile der frühen Aktivierung

Wir haben gesehen, dass die frühe Aktivierung eine Fallstrick hat und dass Sie die meisten ihrer Vorteile nutzen können, ohne sie zu verwenden. Daher ist es besser, die frühe Aktivierung zu vermeiden. Aber ich fühle mich dabei nicht stark und verwende, wie erwähnt, oft Funktionsdeklarationen, weil ich ihre Syntax mag.

11.8.3 Klassendeklarationen werden nicht frühzeitig aktiviert

Obwohl sie in mancher Hinsicht Funktionsdeklarationen ähneln, werden Klassendeklarationen nicht frühzeitig aktiviert

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

Warum ist das so? Betrachten Sie die folgende Klassendeklaration

class MyClass extends Object {}

Der Operand von extends ist ein Ausdruck. Daher können Sie Dinge wie diese tun

const identity = x => x;
class MyClass extends identity(Object) {}

Die Auswertung eines solchen Ausdrucks muss an der Stelle erfolgen, an der er erwähnt wird. Alles andere wäre verwirrend. Das erklärt, warum Klassendeklarationen nicht frühzeitig aktiviert werden.

11.8.4 var: Hoisting (partielle frühe Aktivierung)

var ist eine ältere Art, Variablen zu deklarieren, die vor const und let (die jetzt bevorzugt werden) existiert. Betrachten Sie die folgende var-Deklaration.

var x = 123;

Diese Deklaration hat zwei Teile

Der folgende Code demonstriert die Effekte von var

function f() {
  // Partial early activation:
  assert.equal(x, undefined);
  if (true) {
    var x = 123;
    // The assignment is executed in place:
    assert.equal(x, 123);
  }
  // Scope is function, not block:
  assert.equal(x, 123);
}

11.9 Closures (Funktionen mit Zugriff auf ihren Entstehungskontext)

Bevor wir Closures untersuchen können, müssen wir gebundene und freie Variablen kennenlernen.

11.9.1 Gebundene vs. freie Variablen

Pro Geltungsbereich gibt es eine Menge von Variablen, die erwähnt werden. Unter diesen Variablen unterscheiden wir

Betrachten Sie den folgenden Code

function func(x) {
  const y = 123;
  console.log(z);
}

Im Körper von func() sind x und y gebundene Variablen. z ist eine freie Variable.

11.9.2 Was ist eine Closure?

Was ist also eine Closure?

Eine Closure ist eine Funktion plus eine Verbindung zu den Variablen, die an ihrem „Geburtsort“ existieren.

Was ist der Sinn dieser Verbindung? Sie liefert die Werte für die freien Variablen der Funktion – zum Beispiel

function funcFactory(value) {
  return () => {
    return value;
  };
}

const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)

funcFactory gibt eine Closure zurück, die func zugewiesen wird. Da func die Verbindung zu den Variablen an ihrem Geburtsort hat, kann sie immer noch auf die freie Variable value zugreifen, wenn sie in Zeile A aufgerufen wird (obwohl sie ihren Geltungsbereich „verlassen“ hat).

  Alle Funktionen in JavaScript sind Closures

Statisches Scoping wird in JavaScript über Closures unterstützt. Daher ist jede Funktion eine Closure.

11.9.3 Beispiel: Eine Fabrik für Inkrementierer

Die folgende Funktion gibt Inkrementierer zurück (ein Name, den ich mir gerade ausgedacht habe). Ein Inkrementierer ist eine Funktion, die intern eine Zahl speichert. Wenn sie aufgerufen wird, aktualisiert sie diese Zahl, indem sie das Argument hinzufügt, und gibt den neuen Wert zurück.

function createInc(startValue) {
  return (step) => { // (A)
    startValue += step;
    return startValue;
  };
}
const inc = createInc(5);
assert.equal(inc(2), 7);

Wir können sehen, dass die in Zeile A erstellte Funktion ihre interne Zahl in der freien Variable startValue behält. Diesmal lesen wir nicht nur aus dem Geburtsbereich, sondern wir verwenden ihn, um Daten zu speichern, die wir ändern und die über Funktionsaufrufe hinweg bestehen bleiben.

Wir können weitere Speicherplätze im Geburtsbereich über lokale Variablen erstellen

function createInc(startValue) {
  let index = -1;
  return (step) => {
    startValue += step;
    index++;
    return [index, startValue];
  };
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);

11.9.4 Anwendungsfälle für Closures

Wozu sind Closures gut?

  Quiz: Fortgeschritten

Siehe Quiz-App.