letconstconst und Unveränderlichkeitconst und Schleifenconst und letglobalThis [ES2020]const und let: temporäre tote Zone (temporal dead zone)var: Hoisting (partielle frühe Aktivierung)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.
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;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.',
}
);const und UnveränderlichkeitIn 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.',
}
);const und SchleifenSie 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);
}const und letIch empfehle die folgenden Regeln, um zwischen const und let zu entscheiden
const zeigt eine unveränderliche Bindung an und dass eine Variable niemals ihren Wert ändert. Bevorzugen Sie sie.let zeigt an, dass sich der Wert einer Variablen ändert. Verwenden Sie sie nur, wenn Sie const nicht verwenden können. Übung:
const
exercises/variables-assignment/const_exrc.mjs
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',
}
);x.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.
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.
Alle verbleibenden Abschnitte sind fortgeschritten.
Diese beiden Adjektive beschreiben Phänomene in Programmiersprachen
Betrachten wir Beispiele für diese beiden Begriffe.
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).
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).
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
const, let und Klassendeklarationen erstellt werden.var und Funktionsdeklarationen erstellt.globalThis zugegriffen werden. Es kann verwendet werden, um globale Objektvariablen zu erstellen, zu lesen und zu löschen.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.
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.
globalThisÄltere Methoden für den Zugriff auf das globale Objekt hängen von der Plattform ab
window: ist der klassische Weg, um auf das globale Objekt zu verweisen. Aber es funktioniert nicht in Node.js und in Web Workern.self: ist in Web Workern und Browsern im Allgemeinen verfügbar. Aber es wird von Node.js nicht unterstützt.global: ist nur in Node.js verfügbar.globalThisDas 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
const, let und Klassendeklarationen erstellen keine globalen Objekteigenschaften, wenn sie im globalen Geltungsbereich verwendet werden.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); // yesDaher gibt es relativ wenige Anwendungsfälle für globalThis – zum Beispiel
Dies sind zwei Schlüsselaspekte von Deklarationen
Tabelle 1 fasst zusammen, wie verschiedene Deklarationen mit diesen Aspekten umgehen.
| 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.
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
undefined.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
ReferenceError.undefined gesetzt – wenn kein Initialisierer vorhanden ist.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.
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 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.
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.
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.
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.
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
var x: Der Geltungsbereich einer mit var deklarierten Variablen ist die innerste umgebende Funktion und nicht der innerste umgebende Block, wie bei den meisten anderen Deklarationen. Eine solche Variable ist bereits am Anfang ihres Geltungsbereichs aktiv und mit undefined initialisiert.x = 123: Die Zuweisung wird immer an Ort und Stelle ausgeführt.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);
}Bevor wir Closures untersuchen können, müssen wir gebundene und freie Variablen kennenlernen.
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.
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.
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]);Wozu sind Closures gut?
Zunächst sind sie einfach eine Implementierung von statischem Scoping. Als solche liefern sie Kontextdaten für Callbacks.
Sie können auch von Funktionen verwendet werden, um Zustände zu speichern, die über Funktionsaufrufe hinweg bestehen bleiben. createInc() ist ein Beispiel dafür.
Und sie können private Daten für Objekte bereitstellen (produziert über Literale oder Klassen). Die Details, wie das funktioniert, werden in Exploring ES6 erklärt.
Quiz: Fortgeschritten
Siehe Quiz-App.