Umfang und Kontext in JavaScript sind einzigartig für die Sprache, teilweise dank der Flexibilität, die sie mit sich bringen. Jede Funktion hat einen anderen Variablenkontext und -bereich. Diese Konzepte liegen einigen leistungsstarken Entwurfsmustern in JavaScript zugrunde. Allerdings führt dies auch zu großer Verwirrung bei den Entwicklern. Im Folgenden werden die Unterschiede zwischen Kontext und Umfang in JavaScript umfassend erläutert und wie verschiedene Entwurfsmuster sie verwenden.
Kontext vs. Umfang
Zunächst muss geklärt werden, dass Kontext und Umfang unterschiedliche Konzepte sind. Im Laufe der Jahre ist mir aufgefallen, dass viele Entwickler diese beiden Begriffe oft verwechseln und fälschlicherweise den einen als den anderen bezeichnen. Um fair zu sein, sind diese Begriffe sehr verwirrend geworden.
Mit jedem Funktionsaufruf sind ein Bereich und ein Kontext verknüpft. Grundsätzlich ist der Umfang funktionsbasiert und der Kontext objektbasiert. Mit anderen Worten: Der Umfang bezieht sich auf den Zugriff von Variablen bei jedem Funktionsaufruf, und jeder Aufruf ist unabhängig. Der Kontext ist immer der Wert des Schlüsselworts this, das eine Referenz auf das Objekt ist, das den aktuellen ausführbaren Code aufruft.
Variablenbereich
Variablen können in lokalen oder globalen Bereichen definiert werden, was zu einem Zugriff auf Laufzeitvariablen aus verschiedenen Bereichen führt. Globale Variablen müssen außerhalb des Funktionskörpers deklariert werden, sind während des gesamten laufenden Prozesses vorhanden und können in jedem Bereich aufgerufen und geändert werden. Lokale Variablen werden nur innerhalb des Funktionskörpers definiert und haben für jeden Funktionsaufruf einen anderen Gültigkeitsbereich. Dieses Thema befasst sich nur mit der Zuweisung, Auswertung und Bedienung von Werten innerhalb des Aufrufs. Auf Werte außerhalb des Bereichs kann nicht zugegriffen werden.
Derzeit unterstützt JavaScript keinen Gültigkeitsbereich auf Blockebene. Der Gültigkeitsbereich auf Blockebene bezieht sich auf die Definition von Variablen in Anweisungsblöcken, z. B. if-Anweisungen, Switch-Anweisungen, Schleifenanweisungen usw. Das bedeutet, dass auf Variablen außerhalb des Anweisungsblocks nicht zugegriffen werden kann. Derzeit kann auf alle innerhalb eines Anweisungsblocks definierten Variablen außerhalb des Anweisungsblocks zugegriffen werden. Dies wird sich jedoch bald ändern, da das Schlüsselwort let offiziell zur ES6-Spezifikation hinzugefügt wurde. Verwenden Sie es anstelle des Schlüsselworts var, um lokale Variablen als Gültigkeitsbereich auf Blockebene zu deklarieren.
„diesen“ Kontext
Der Kontext hängt normalerweise davon ab, wie eine Funktion aufgerufen wird. Wenn eine Funktion als Methode für ein Objekt aufgerufen wird, wird dies auf das Objekt festgelegt, für das die Methode aufgerufen wird:
Kopieren Sie den Codecode wie folgt:
var-Objekt = {
foo: function(){
alarm(this === object);
}
};
object.foo(); // true
Das gleiche Prinzip gilt beim Aufrufen einer Funktion zum Erstellen einer Instanz eines Objekts mithilfe des neuen Operators. Bei diesem Aufruf wird der Wert auf die neu erstellte Instanz gesetzt:
Kopieren Sie den Codecode wie folgt:
Funktion foo(){
alarm(this);
}
foo() // Fenster
new foo() // foo
Beim Aufruf einer ungebundenen Funktion wird diese standardmäßig auf den globalen Kontext oder das Fensterobjekt (sofern in einem Browser) festgelegt. Wenn die Funktion jedoch im strikten Modus („use strict“) ausgeführt wird, wird der Wert dieser Funktion standardmäßig auf undefiniert gesetzt.
Ausführungskontext und Umfangskette
JavaScript ist eine Single-Threaded-Sprache, was bedeutet, dass sie jeweils nur eine Aufgabe im Browser ausführen kann. Wenn der JavaScript-Interpreter zum ersten Mal Code ausführt, verwendet er zunächst standardmäßig den globalen Kontext. Jeder Aufruf einer Funktion erstellt einen neuen Ausführungskontext.
Hier kommt es häufig zu Verwirrung. Der Begriff „Ausführungskontext“ bedeutet hier den Umfang und nicht den Kontext, wie oben erläutert. Dies ist eine schlechte Benennung, aber der Begriff wird durch die ECMAScript-Spezifikation definiert und es bleibt keine andere Wahl, als sich daran zu halten.
Jedes Mal, wenn ein neuer Ausführungskontext erstellt wird, wird er an der Spitze der Bereichskette hinzugefügt und wird zum Ausführungs- oder Aufrufstapel. Der Browser wird immer im aktuellen Ausführungskontext am Anfang der Bereichskette ausgeführt. Sobald er abgeschlossen ist, wird er (der aktuelle Ausführungskontext) von der Oberseite des Stapels entfernt und die Steuerung wird an den vorherigen Ausführungskontext zurückgegeben. Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
Funktion zuerst(){
zweite();
Funktion Sekunde(){
dritte();
Funktion Third(){
vierte();
Funktion vierter(){
//etwas tun
}
}
}
}
Erste();
Durch Ausführen des vorherigen Codes werden die verschachtelten Funktionen von oben nach unten bis zur vierten Funktion ausgeführt. Zu diesem Zeitpunkt lautet die Bereichskette von oben nach unten: vierte, dritte, zweite, erste, globale. Die vierte Funktion kann wie ihre eigenen Variablen auf globale Variablen und alle in der ersten, zweiten und dritten Funktion definierten Variablen zugreifen. Sobald die Ausführung der vierten Funktion abgeschlossen ist, wird der vierte Kontext vom oberen Ende der Bereichskette entfernt und die Ausführung kehrt zur dritten Funktion zurück. Dieser Vorgang wird fortgesetzt, bis die Ausführung des gesamten Codes abgeschlossen ist.
Konflikte bei der Benennung von Variablen zwischen verschiedenen Ausführungskontexten werden gelöst, indem die Bereichskette von lokal nach global aufsteigt. Dies bedeutet, dass lokale Variablen mit demselben Namen in der Bereichskette eine höhere Priorität haben.
Einfach ausgedrückt: Jedes Mal, wenn Sie versuchen, auf eine Variable im Funktionsausführungskontext zuzugreifen, beginnt der Suchvorgang immer beim eigenen Variablenobjekt. Wenn die gesuchte Variable nicht in Ihrem eigenen Variablenobjekt gefunden wird, fahren Sie mit der Suche in der Bereichskette fort. Es erklimmt die Bereichskette und untersucht jedes Ausführungskontextvariablenobjekt, um einen Wert zu finden, der mit dem Variablennamen übereinstimmt.
Schließung
Ein Abschluss wird gebildet, wenn auf eine verschachtelte Funktion außerhalb ihrer Definition (Bereich) zugegriffen wird, sodass sie nach der Rückkehr der äußeren Funktion ausgeführt werden kann. Es (der Abschluss) verwaltet (in der inneren Funktion) den Zugriff auf lokale Variablen, Argumente und Funktionsdeklarationen in der äußeren Funktion. Durch die Kapselung können wir den Ausführungskontext vor dem äußeren Bereich verbergen und schützen und gleichzeitig die öffentliche Schnittstelle offenlegen, über die weitere Vorgänge ausgeführt werden können. Ein einfaches Beispiel sieht so aus:
Kopieren Sie den Codecode wie folgt:
Funktion foo(){
var local = 'private Variable';
Rückgabefunktion bar(){
lokal zurückgeben;
}
}
var getLocalVariable = foo();
getLocalVariable() // private Variable
Eine der beliebtesten Verschlussarten ist das bekannte Modulmuster. Es ermöglicht Ihnen, öffentliche, private und privilegierte Mitglieder zu verspotten:
Kopieren Sie den Codecode wie folgt:
var Module = (function(){
var privateProperty = 'foo';
Funktion privateMethod(args){
//etwas tun
}
zurückkehren {
publicProperty: "",
publicMethod: function(args){
//etwas tun
},
privilegedMethod: function(args){
privateMethod(args);
}
}
})();
Module ähneln tatsächlich etwas Singletons, indem sie am Ende ein Paar Klammern hinzufügen und sie sofort ausführen, nachdem der Interpreter mit der Interpretation fertig ist (führen Sie die Funktion sofort aus). Die einzigen verfügbaren externen Mitglieder des Abschlussausführungskontexts sind die öffentlichen Methoden und Eigenschaften im zurückgegebenen Objekt (z. B. Module.publicMethod). Allerdings bleiben alle privaten Eigenschaften und Methoden während des gesamten Lebenszyklus des Programms bestehen, da der Ausführungskontext geschützt ist (Abschlüsse) und die Interaktion mit Variablen über öffentliche Methoden erfolgt.
Eine andere Art des Abschlusses wird als sofort aufgerufener Funktionsausdruck IIFE bezeichnet, der nichts anderes als eine selbstaufgerufene anonyme Funktion im Fensterkontext ist.
Kopieren Sie den Codecode wie folgt:
Funktion(Fenster){
var a = 'foo', b = 'bar';
Funktion privat(){
//etwas tun
}
window.Module = {
public: function(){
//etwas tun
}
};
})(Das);
Dieser Ausdruck ist sehr nützlich, um den globalen Namespace zu schützen. Alle im Funktionskörper deklarierten Variablen sind lokale Variablen und bleiben durch Schließungen in der gesamten Laufzeitumgebung bestehen. Diese Art der Kapselung von Quellcode ist sowohl bei Programmen als auch bei Frameworks sehr beliebt und stellt normalerweise eine einzige globale Schnittstelle für die Interaktion mit der Außenwelt bereit.
Rufen Sie an und bewerben Sie sich
Diese beiden einfachen Methoden, die in alle Funktionen integriert sind, ermöglichen die Ausführung von Funktionen in einem benutzerdefinierten Kontext. Die Aufruffunktion erfordert eine Parameterliste, während die Apply-Funktion Ihnen die Übergabe der Parameter als Array ermöglicht:
Kopieren Sie den Codecode wie folgt:
Funktion Benutzer(erster, letzter, Alter){
//etwas tun
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);
Das Ergebnis der Ausführung ist dasselbe: Die Benutzerfunktion wird im Fensterkontext aufgerufen und es werden dieselben drei Parameter bereitgestellt.
ECMAScript 5 (ES5) führte die Methode Function.prototype.bind zur Kontextsteuerung ein, die eine neue Funktion zurückgibt, die dauerhaft an das erste Argument der Bind-Methode gebunden ist, unabhängig davon, wie die Funktion aufgerufen wird. Es korrigiert den Kontext der Funktion durch Schließungen. Hier ist eine Lösung für Browser, die dies nicht unterstützen:
Kopieren Sie den Codecode wie folgt:
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1);
Rückgabefunktion(){
return fn.apply(context, args);
}
}
}
Es wird häufig bei Kontextverlust verwendet: objektorientiert und Ereignisverarbeitung. Dies ist notwendig, da die addEventListener-Methode des Knotens den Ausführungskontext der Funktion immer als Knoten beibehält, an den der Ereignishandler gebunden ist, was wichtig ist. Wenn Sie jedoch fortgeschrittene objektorientierte Techniken verwenden und den Kontext der Rückruffunktion als Instanz der Methode beibehalten müssen, müssen Sie den Kontext manuell anpassen. Das ist der Komfort, den bind mit sich bringt:
Kopieren Sie den Codecode wie folgt:
Funktion MyClass(){
this.element = document.createElement('div');
this.element.addEventListener('click', this.onClick.bind(this), false);
}
MyClass.prototype.onClick = function(e){
//etwas tun
};
Wenn Sie auf den Quellcode der Bindefunktion zurückblicken, fällt Ihnen möglicherweise die folgende relativ einfache Codezeile auf, die eine Methode für ein Array aufruft:
Kopieren Sie den Codecode wie folgt:
Array.prototype.slice.call(arguments, 1);
Interessanterweise ist hier zu beachten, dass das Argumentobjekt eigentlich kein Array ist, es wird jedoch häufig als arrayähnliches Objekt beschrieben, ähnlich wie die Knotenliste (das von der Methode document.getElementsByTagName() zurückgegebene Ergebnis). Sie enthalten Längenattribute und die Werte können indiziert werden, sind jedoch immer noch keine Arrays, da sie keine nativen Array-Methoden wie Slice und Push unterstützen. Da sie sich jedoch ähnlich wie Arrays verhalten, können Array-Methoden aufgerufen und gekapert werden. Wenn Sie Array-Methoden in einem Array-ähnlichen Kontext ausführen möchten, folgen Sie dem obigen Beispiel.
Diese Technik zum Aufrufen von Methoden anderer Objekte wird auch objektorientiert angewendet, wenn die klassische Vererbung (Klassenvererbung) in JavaScript emuliert wird:
Kopieren Sie den Codecode wie folgt:
MyClass.prototype.init = function(){
// Rufen Sie die Init-Methode der Superklasse im Kontext der Instanz „MyClass“ auf
MySuperClass.prototype.init.apply(this, arguments);
}
Wir können dieses leistungsstarke Entwurfsmuster reproduzieren, indem wir Methoden der Oberklasse (MySuperClass) in Instanzen der Unterklasse (MyClass) aufrufen.
abschließend
Es ist sehr wichtig, diese Konzepte zu verstehen, bevor Sie mit dem Erlernen fortgeschrittener Entwurfsmuster beginnen, da Umfang und Kontext im modernen JavaScript eine wichtige und grundlegende Rolle spielen. Ob wir über Abschlüsse, objektorientiert und Vererbung oder verschiedene native Implementierungen sprechen, Kontext und Umfang spielen eine wichtige Rolle. Wenn Ihr Ziel darin besteht, die JavaScript-Sprache zu beherrschen und ein tiefes Verständnis ihrer Komponenten zu erlangen, sollten Umfang und Kontext Ihr Ausgangspunkt sein.
Beilage für Übersetzer
Die vom Autor implementierte Bind-Funktion ist unvollständig. Parameter können beim Aufruf der von Bind zurückgegebenen Funktion nicht übergeben werden.
Kopieren Sie den Codecode wie folgt:
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1);
Rückgabefunktion(){
return fn.apply(context, args.concat(arguments));//fixed
}
}
}