Close

7. Mai 2017

Das Ding mit dem this in JavaScript mal einfach

Wir können etwa vier Wege in JavaScript finden, eine Referenz zu this zu definieren. this wird je nach Kontext nämlich dynamisch aufgelöst und ist, anders als in den meisten (objekt-)typisierten Sprachen mit klassischen Meta-/Definitions-Konstrukten (aka „Klassen“), nicht immer die Referenz zum ursprünglichen Objekt selbst. Da jedoch genau dieser Kontext häufig komplex ist und somit Referenzen (speziell in anonymen Funktionen und Closures) gerne kopiert werden, um die Interpretation von this zu fixieren, hierbei jedoch bestimmte Optimierungen der JavaScript Engines unmöglich oder komplexer werden, hier eine (hoffentlich) einfache Erklärung, welchen Kontext this wann annehmen wird und wie der Kontext re-definiert werden kann.

0. Das Problem

this im erweiterten Kontext, beispielsweise einer Callback-Funktion, kann nicht (immer) statisch vorhergesehen werden. Hierbei kann this zwar den ursprünglichen Kontext zurück erhalten (falls die auslösende Aktion diesen definiert), jedoch kann this auch einen eigenen oder fremden Kontext referenzieren. Hierbei kann man zuweilen auf die Lösung einer kopierten Referenz auf ein self, this_, context, … stoßen:


function GlobalContext ()
{
   var self = this;

   this.doAnything = function () { return "anything" };

   var innerContext = function ()
   {
      self.doAnything();
   }

   document.addEventListener('onDomReady', innerContext);

   return this;
}

Grundsätzlich ist dies zwar eine Möglichkeit der Dynamik von this Herr zu werden, verhindert jedoch im selben Atemzug weitergehende Optimierungen durch JavaScript Engines und verringert ebenso die Lesbarkeit des eigenen Codes (aber das ist ja auch Geschmackssache 😉 ).

Nun aber zu den möglichen Kontext-Definitionen von this:

1. Im lokalen Kontext

Wird eine Methode im lokalen Kontext aufgerufen, so wird this auf den selben Kontext angewendet und referenziert diesen.


var myObj = new GlobalContext();
var mySecondObj = new GlobalContext();

myObj.doAnything(); // this referenziert myObj
mySecondObj['doAnything'](); // this referenziert mySecondObj

2. Im globalen Kontext

Wird eine Methode direkt aufgerufen, referenziert this, anders als möglicherweise erwartet, nicht den lokalen Kontext der jeweiligen Methode, sondern den globalen Kontext (im Browser also window, in NodeJS das entsprechende global Objekt).


var testFunc = function ()
{
   return this === window; // true
}

3. Im explizit definierten Kontext

this als solches kann schon aufgrund der Dynamik in sich explizit definiert werden. Hierbei seien die Vertreter der explizit definierten Referenz call und apply erwähnt, die beide ähnliche Effekte mit unterschiedlicher Performance liefern.


var newThis = { key: "value", key2: 12345 }

function exampleContext (param1)
{
   return (this === window) || (this === global)
}

exampleContext(); // true
exampleContext.call(undefined, "paramValue"); // true, null || undefined referenzieren hier window, solange kein strict mode Anwendung findet

exampleContext.call(newThis);
exampleContext.call(newThis, "paramValue"); // false, this referenziert in beiden Fällen window.newThis bzw. global.newThis

 

4. Im explizit definierten und gebundenen Kontext

Um die Eigenschaften des Punktes 3, nämlich der explizit definierten Form von this entgegen zu wirken, existiert eine gebundene Form von this, die den Kontext statisch an die jeweilige Methodendefinition bindet und ein Überschreiben der Referenz auf this unterbindet.

Ändern wir also unsere GlobalContext.doAnything und „binden“ diese auf this in der „Klassendefinition“, so wird this immer auf GlobalContext referenzieren. Etwa so:


function GlobalContext ()
{
   this.foo = "bar";

   this.doAnything = function () { console.log("bound method", this.foo) }.bind(this);

   this.doAnotherThing = function () { console.log("unbound method", this.foo) };

   document.addEventListener('onDomReady', doAnything);
   document.addEventListener('onDomReady', doAnotherThing);

   return this;
}

Hier wird nun in der Konsole zu „bound method“ „bar“ ausgegeben, sobald das DOM ready ist. doAnotherThing wird stattdessen „unbound method“ undefined ausgeben. Ohne unseren bind erhalten wir also als this die Referenz zum Callee, also der aufrufenden Funktion, statt der definierenden Klasse (was wir aus statisch typisierten Sprachen gewohnt wären).

Bonus: ES6/Next & React

In React .render-Methoden ist es ein recht gängiges Pattern Methoden oder Callbacks zu binden, auch in ES6 Klassen bleibt uns ein Binding nicht erspart.
Für Fat-Arrow Funktionen gilt; this ist immer auf das außenliegende gebunden, in den meisten Fällen kann man sich hier als das binding auf this sparen.


// ... any react class
// Pattern here if we want to mutate the state of the Component; bind the setClicked to it, otherwise we can't access this.setState in the Component.
render() {
   return <CustomBtn onClick={this.setClicked.bind(this)} />;
}

Und im Vergleich mit einer Fat-Arrow Function


// ... any react class
// Pattern here we pass the click event over to setClicked, setClicked is auto-bound to what is this in render (so the Component)
render() {
   return <CustomBtn onClick={(ev) => this.setClicked(ev)} />;
}

 

Image credits: VIKTOR HANACEK (modifiziert).

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.