Bei der Übergabe von Objektmethoden als Rückrufe, beispielsweise an setTimeout
, gibt es ein bekanntes Problem: „ this
geht verloren“.
In diesem Kapitel erfahren Sie, wie Sie das Problem beheben können.
Wir haben bereits Beispiele dafür gesehen, dass this
verloren gegangen ist. Sobald eine Methode irgendwo getrennt vom Objekt übergeben wird, geht this
verloren.
So kann es mit setTimeout
passieren:
let user = { Vorname: „John“, sayHi() { Alert(`Hallo, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Hallo, undefiniert!
Wie wir sehen können, zeigt die Ausgabe nicht „John“ als this.firstName
, sondern undefined
!
Das liegt daran, dass setTimeout
die Funktion user.sayHi
separat vom Objekt erhalten hat. Die letzte Zeile kann wie folgt umgeschrieben werden:
let f = user.sayHi; setTimeout(f, 1000); // Benutzerkontext verloren
Die Methode setTimeout
im Browser ist etwas Besonderes: Sie setzt this=window
für den Funktionsaufruf (für Node.js wird this
zum Timer-Objekt, spielt hier aber keine Rolle). Für this.firstName
wird also versucht, window.firstName
abzurufen, das nicht existiert. In anderen ähnlichen Fällen wird this
normalerweise einfach undefined
.
Die Aufgabe ist recht typisch – wir wollen eine Objektmethode an eine andere Stelle (hier – an den Scheduler) übergeben, wo sie aufgerufen wird. Wie kann sichergestellt werden, dass es im richtigen Kontext aufgerufen wird?
Die einfachste Lösung ist die Verwendung einer Wrapping-Funktion:
let user = { Vorname: „John“, sayHi() { Alert(`Hallo, ${this.firstName}!`); } }; setTimeout(function() { user.sayHi(); // Hallo, John! }, 1000);
Jetzt funktioniert es, weil es user
aus der äußeren lexikalischen Umgebung empfängt und die Methode dann normal aufruft.
Das Gleiche, aber kürzer:
setTimeout(() => user.sayHi(), 1000); // Hallo, John!
Sieht gut aus, aber in unserer Codestruktur tritt eine leichte Sicherheitslücke auf.
Was passiert, wenn user
den Wert ändert, bevor setTimeout
ausgelöst wird (es gibt eine Verzögerung von einer Sekunde)? Dann wird plötzlich das falsche Objekt aufgerufen!
let user = { Vorname: „John“, sayHi() { Alert(`Hallo, ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...der Wert von user ändert sich innerhalb von 1 Sekunde Benutzer = { sayHi() { Alert("Ein anderer Benutzer in setTimeout!"); } }; // Ein anderer Benutzer in setTimeout!
Die nächste Lösung garantiert, dass so etwas nicht passieren wird.
Funktionen bieten eine integrierte Methode bind, mit der this
behoben werden kann.
Die grundlegende Syntax lautet:
// Eine komplexere Syntax folgt etwas später letboundFunc = func.bind(context);
Das Ergebnis von func.bind(context)
ist ein spezielles funktionsähnliches „exotisches Objekt“, das als Funktion aufrufbar ist und den Aufruf transparent an func
übergibt, indem es this=context
setzt.
Mit anderen Worten: Der Aufruf boundFunc
ist wie func
mit „fixed this
.
Hier übergibt funcUser
beispielsweise einen Aufruf an func
mit this=user
:
let user = { Vorname: „John“ }; Funktion func() { alarm(this.firstName); } let funcUser = func.bind(user); funcUser(); // John
Hier func.bind(user)
als „gebundene Variante“ von func
, mit festem this=user
.
Alle Argumente werden „wie sie sind“ an die ursprüngliche func
übergeben, zum Beispiel:
let user = { Vorname: „John“ }; Funktion func(phrase) { alarm(phrase + ', ' + this.firstName); } // Dies an den Benutzer binden let funcUser = func.bind(user); funcUser("Hallo"); // Hallo, John (Argument „Hallo“ wird übergeben und this=user)
Versuchen wir es nun mit einer Objektmethode:
let user = { Vorname: „John“, sayHi() { Alert(`Hallo, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // kann es ohne Objekt ausführen sayHi(); // Hallo, John! setTimeout(sayHi, 1000); // Hallo, John! // auch wenn sich der Wert von user innerhalb von 1 Sekunde ändert // sayHi verwendet den vorgebundenen Wert, der auf das alte Benutzerobjekt verweist Benutzer = { sayHi() { Alert("Ein anderer Benutzer in setTimeout!"); } };
In der Zeile (*)
nehmen wir die Methode user.sayHi
und binden sie an user
. Das sayHi
ist eine „gebundene“ Funktion, die einzeln aufgerufen oder an setTimeout
übergeben werden kann – egal, der Kontext wird stimmen.
Hier können wir sehen, dass Argumente „wie sie sind“ übergeben werden, nur this
durch bind
behoben wird:
let user = { Vorname: „John“, say(phrase) { Alert(`${phrase}, ${this.firstName}!`); } }; let say = user.say.bind(user); say("Hallo"); // Hallo, John! („Hallo“-Argument wird übergeben, um zu sagen) say("Tschüs"); // Tschüss, John! („Bye“ wird verabschiedet)
Komfortmethode: bindAll
Wenn ein Objekt über viele Methoden verfügt und wir planen, es aktiv weiterzugeben, können wir sie alle in einer Schleife binden:
for (Benutzer eingeben lassen) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } }
JavaScript-Bibliotheken bieten auch Funktionen für eine bequeme Massenbindung, z. B. _.bindAll(object, methodNames) in lodash.
Bisher haben wir nur davon gesprochen, this
zu binden. Gehen wir noch einen Schritt weiter.
Wir können nicht nur this
, sondern auch Argumente binden. Das wird selten gemacht, kann aber manchmal praktisch sein.
Die vollständige Syntax von bind
:
letbound = func.bind(context, [arg1], [arg2], ...);
Es ermöglicht das Binden des Kontexts als this
und der Startargumente der Funktion.
Zum Beispiel haben wir eine Multiplikationsfunktion mul(a, b)
:
Funktion mul(a, b) { return a * b; }
Lassen Sie uns bind
verwenden, um auf seiner Basis eine Funktion double
zu erstellen:
Funktion mul(a, b) { return a * b; } let double = mul.bind(null, 2); alarm( double(3) ); // = mul(2, 3) = 6 alarm( double(4) ); // = mul(2, 4) = 8 alarm( double(5) ); // = mul(2, 5) = 10
Der Aufruf von mul.bind(null, 2)
erstellt eine neue Funktion double
die Aufrufe an mul
übergibt und dabei null
als Kontext und 2
als erstes Argument festlegt. Weitere Argumente werden „wie sie sind“ übergeben.
Das nennt man Teilfunktionsanwendung – wir erstellen eine neue Funktion, indem wir einige Parameter der vorhandenen Funktion korrigieren.
Bitte beachten Sie, dass wir this
hier eigentlich nicht verwenden. Aber bind
erfordert es, also müssen wir so etwas wie null
eingeben.
Die Funktion triple
im folgenden Code verdreifacht den Wert:
Funktion mul(a, b) { return a * b; } let Triple = mul.bind(null, 3); Alert( Triple(3) ); // = mul(3, 3) = 9 Alert( Triple(4) ); // = mul(3, 4) = 12 Alert( Triple(5) ); // = mul(3, 5) = 15
Warum erstellen wir normalerweise eine Teilfunktion?
Der Vorteil besteht darin, dass wir eine unabhängige Funktion mit einem lesbaren Namen ( double
, triple
) erstellen können. Wir können es verwenden und müssen nicht jedes Mal das erste Argument angeben, da es mit bind
behoben wird.
In anderen Fällen ist eine teilweise Anwendung nützlich, wenn wir eine sehr generische Funktion haben und der Einfachheit halber eine weniger universelle Variante davon wünschen.
Zum Beispiel haben wir eine Funktion send(from, to, text)
. Dann möchten wir innerhalb eines user
möglicherweise eine Teilvariante davon verwenden: sendTo(to, text)
das vom aktuellen Benutzer sendet.
Was wäre, wenn wir einige Argumente korrigieren möchten, aber nicht den this
? Zum Beispiel für eine Objektmethode.
Die native bind
lässt das nicht zu. Wir können nicht einfach den Kontext weglassen und zu den Argumenten springen.
Glücklicherweise kann eine partial
zum Binden nur von Argumenten leicht implementiert werden.
So was:
Funktion partiell(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // Verwendung: let user = { Vorname: „John“, say(Zeit, Phrase) { Alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // Eine Teilmethode mit fester Zeit hinzufügen user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hallo"); // Etwas wie: // [10:00] John: Hallo!
Das Ergebnis des Aufrufs partial(func[, arg1, arg2...])
ist ein Wrapper (*)
, der func
aufruft mit:
this
wie es nur geht (für user.sayNow
nennen Sie es user
)
Dann gibt es ...argsBound
– Argumente aus dem partial
( "10:00"
)
Dann gibt es ...args
– Argumente, die dem Wrapper übergeben werden ( "Hello"
)
So einfach geht das mit der Spread-Syntax, oder?
Außerdem gibt es eine fertige _.partial-Implementierung aus der Lodash-Bibliothek.
Die Methode func.bind(context, ...args)
gibt eine „gebundene Variante“ der Funktion func
zurück, die den Kontext this
und der ersten Argumente, falls angegeben, festlegt.
Normalerweise wenden wir bind
an, um this
für eine Objektmethode zu beheben, sodass wir es irgendwo übergeben können. Zum Beispiel auf setTimeout
.
Wenn wir einige Argumente einer vorhandenen Funktion korrigieren, wird die resultierende (weniger universelle) Funktion als teilweise angewendet oder teilweise bezeichnet.
Teilweise sind praktisch, wenn wir das gleiche Argument nicht immer wieder wiederholen möchten. Wenn wir beispielsweise eine send(from, to)
haben und from
für unsere Aufgabe immer gleich sein sollte, können wir einen Teil erhalten und damit fortfahren.
Wichtigkeit: 5
Was wird die Ausgabe sein?
Funktion f() { alarm( this ); // ? } let user = { g: f.bind(null) }; user.g();
Die Antwort: null
.
Funktion f() { alarm( this ); // null } let user = { g: f.bind(null) }; user.g();
Der Kontext einer gebundenen Funktion ist fest festgelegt. Es gibt einfach keine Möglichkeit, es weiter zu ändern.
Selbst während wir user.g()
ausführen, wird die ursprüngliche Funktion mit this=null
aufgerufen.
Wichtigkeit: 5
Können wir this
durch zusätzliche Bindung ändern?
Was wird die Ausgabe sein?
Funktion f() { alarm(this.name); } f = f.bind( {name: „John“} ).bind( {name: „Ann“ } ); F();
Die Antwort: John .
Funktion f() { alarm(this.name); } f = f.bind( {name: „John“} ).bind( {name: „Pete“} ); F(); // John
Das von f.bind(...)
zurückgegebene exotisch gebundene Funktionsobjekt merkt sich den Kontext (und die Argumente, falls angegeben) nur zum Zeitpunkt der Erstellung.
Eine Funktion kann nicht erneut gebunden werden.
Wichtigkeit: 5
Die Eigenschaft einer Funktion enthält einen Wert. Wird es sich nach bind
ändern? Warum oder warum nicht?
Funktion sayHi() { alarm( this.name ); } sayHi.test = 5; letbound = sayHi.bind({ Name: „John“ }); alarm(bound.test); // Was wird die Ausgabe sein? Warum?
Die Antwort: undefined
.
Das Ergebnis von bind
ist ein weiteres Objekt. Es verfügt nicht über die test
.
Wichtigkeit: 5
Der Aufruf von askPassword()
im folgenden Code sollte das Passwort überprüfen und dann abhängig von der Antwort user.loginOk/loginFail
aufrufen.
Aber es führt zu einem Fehler. Warum?
Korrigieren Sie die hervorgehobene Zeile, damit alles ordnungsgemäß funktioniert (andere Zeilen dürfen nicht geändert werden).
Funktion askPassword(ok, fail) { let password = prompt("Passwort?", ''); if (password == "rockstar") ok(); sonst scheitern(); } let user = { Name: 'John', loginOk() { Alert(`${this.name} angemeldet`); }, loginFail() { Alert(`${this.name} konnte sich nicht anmelden`); }, }; askPassword(user.loginOk, user.loginFail);
Der Fehler tritt auf, weil askPassword
die Funktionen loginOk/loginFail
ohne das Objekt erhält.
Wenn es sie aufruft, gehen sie natürlich davon aus this=undefined
.
Lassen Sie uns den Kontext bind
:
Funktion askPassword(ok, fail) { let password = prompt("Passwort?", ''); if (password == "rockstar") ok(); sonst scheitern(); } let user = { Name: 'John', loginOk() { Alert(`${this.name} angemeldet`); }, loginFail() { Alert(`${this.name} konnte sich nicht anmelden`); }, }; askPassword(user.loginOk.bind(user), user.loginFail.bind(user));
Jetzt funktioniert es.
Eine alternative Lösung könnte sein:
//...... askPassword(() => user.loginOk(), () => user.loginFail());
Normalerweise funktioniert das auch und sieht gut aus.
In komplexeren Situationen, in denen sich user
möglicherweise ändert, nachdem askPassword
aufgerufen wurde, aber bevor der Besucher antwortet und () => user.loginOk()
aufruft, ist dies etwas weniger zuverlässig.
Wichtigkeit: 5
Die Aufgabe ist eine etwas komplexere Variante von Reparieren einer Funktion, die „dies“ verliert.
Das user
wurde geändert. Anstelle von zwei Funktionen loginOk/loginFail
gibt es jetzt eine einzige Funktion user.login(true/false)
.
Was sollen wir askPassword
im folgenden Code übergeben, damit user.login(true)
als ok
und user.login(false)
als fail
aufgerufen wird?
Funktion askPassword(ok, fail) { let password = prompt("Passwort?", ''); if (password == "rockstar") ok(); sonst scheitern(); } let user = { Name: 'John', login(result) { Alert( this.name + (Ergebnis ? 'angemeldet' : 'Anmeldung fehlgeschlagen') ); } }; askPassword(?, ?); // ?
Ihre Änderungen sollten nur das hervorgehobene Fragment ändern.
Verwenden Sie entweder eine Wrapper-Funktion, um es kurz zu machen, einen Pfeil:
askPassword(() => user.login(true), () => user.login(false));
Jetzt ruft es user
von äußeren Variablen ab und führt es auf normale Weise aus.
Oder erstellen Sie eine Teilfunktion aus user.login
, die user
als Kontext verwendet und das richtige erste Argument hat:
askPassword(user.login.bind(user, true), user.login.bind(user, false));