JavaScript: Was bedeutet das?

Es kann schwierig sein, den Wert von this in JavaScript zu ermitteln. Hier ist eine Anleitung:

Jake Archibald
Jake Archibald

Die this von JavaScript ist oft Gegenstand von Witzen, weil sie ziemlich kompliziert ist. Ich habe jedoch schon Entwickler gesehen, die viel kompliziertere und domänenspezifische Dinge getan haben, um diese this zu vermeiden. Wenn du dir nicht sicher bist, was this bedeutet, hoffen wir, dass dir diese Informationen weiterhelfen. Dies ist mein this-Leitfaden.

Ich beginne mit der spezifischen Situation und ende mit der am wenigsten spezifischen. Dieser Artikel ist ein bisschen wie ein großes if (…) … else if () … else if (…) …. Sie können also direkt zum ersten Abschnitt springen, der dem Code entspricht, den Sie sich ansehen.

  1. Wenn die Funktion als Pfeilfunktion definiert ist
  2. Andernfalls, wenn die Funktion/Klasse mit new aufgerufen wird
  3. Andernfalls, wenn die Funktion einen gebundenen this-Wert hat
  4. Andernfalls, wenn this zum Zeitpunkt des Anrufs festgelegt ist
  5. Andernfalls, wenn die Funktion über ein übergeordnetes Objekt (parent.func()) aufgerufen wird
  6. Andernfalls, wenn die Funktion oder der übergeordnete Bereich sich im strengen Modus befindet
  7. Andernfalls

Wenn die Funktion als Pfeilfunktion definiert ist:

const arrowFunction = () => {
  console
.log(this);
};

In diesem Fall ist der Wert von this immer mit this im übergeordneten Bereich identisch:

const outerThis = this;

const arrowFunction = () => {
 
// Always logs `true`:
  console
.log(this === outerThis);
};

Pfeilfunktionen sind sehr praktisch, da der innere Wert von this nicht geändert werden kann. Er ist immer gleich wie der äußere this.

Weitere Beispiele

Bei Pfeilfunktionen kann der Wert von this nicht mit bind geändert werden:

// Logs `true` - bound `this` value is ignored:
arrowFunction
.bind({foo: 'bar'})();

Bei Pfeilfunktionen kann der Wert von this nicht mit call oder apply geändert werden:

// Logs `true` - called `this` value is ignored:
arrowFunction
.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction
.apply({foo: 'bar'});

Bei Pfeilfunktionen kann der Wert von this nicht geändert werden, indem die Funktion als Mitglied eines anderen Objekts aufgerufen wird:

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj
.arrowFunction();

Bei Pfeilfunktionen kann der Wert von this nicht geändert werden, indem die Funktion als Konstruktor aufgerufen wird:

// TypeError: arrowFunction is not a constructor
new arrowFunction();

„Gebundene“ Instanzmethoden

Wenn Sie bei Instanzmethoden dafür sorgen möchten, dass sich this immer auf die Klasseninstanz bezieht, sollten Sie Pfeilfunktionen und Klassenfelder verwenden:

class Whatever {
  someMethod
= () => {
   
// Always the instance of Whatever:
    console
.log(this);
 
};
}

Dieses Muster ist sehr nützlich, wenn Instanzmethoden als Ereignislistener in Komponenten verwendet werden (z. B. React- oder Webkomponenten).

Das oben genannte Beispiel scheint die Regel zu verletzen, dass this mit this im übergeordneten Gültigkeitsbereich identisch sein muss. Es ergibt aber Sinn, wenn Sie Klassenfelder als syntaktischen Zucker für das Festlegen von Elementen im Konstruktor betrachten:

class Whatever {
  someMethod
= (() => {
   
const outerThis = this;
   
return () => {
     
// Always logs `true`:
      console
.log(this === outerThis);
   
};
 
})();
}

// …is roughly equivalent to:

class Whatever {
  constructor
() {
   
const outerThis = this;
   
this.someMethod = () => {
     
// Always logs `true`:
      console
.log(this === outerThis);
   
};
 
}
}

Alternative Muster beinhalten das Binden einer vorhandenen Funktion im Konstruktor oder die Zuweisung der Funktion im Konstruktor. Wenn Sie aus irgendeinem Grund keine Klassenfelder verwenden können, ist die Zuweisung von Funktionen im Konstruktor eine angemessene Alternative:

class Whatever {
  constructor
() {
   
this.someMethod = () => {
     
// …
   
};
 
}
}

Andernfalls, wenn die Funktion/Klasse mit new aufgerufen wird:

new Whatever();

Dadurch wird Whatever (oder die Konstruktorfunktion, falls es sich um eine Klasse handelt) mit this als dem Ergebnis von Object.create(Whatever.prototype) aufgerufen.

class MyClass {
  constructor
() {
    console
.log(
     
this.constructor === Object.create(MyClass.prototype).constructor,
   
);
 
}
}

// Logs `true`:
new MyClass();

Dasselbe gilt für ältere Konstruktoren:

function MyClass() {
  console
.log(
   
this.constructor === Object.create(MyClass.prototype).constructor,
 
);
}

// Logs `true`:
new MyClass();

Weitere Beispiele

Wenn new aufgerufen wird, kann der Wert von this nicht mit bind geändert werden:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

Wenn die Funktion mit new aufgerufen wird, kann der Wert von this nicht geändert werden, indem die Funktion als Mitglied eines anderen Objekts aufgerufen wird:

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

Andernfalls, wenn die Funktion einen this-Wert vom Typ „bound“ hat:

function someFunction() {
 
return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

Wenn boundFunction aufgerufen wird, ist sein this-Wert das Objekt, das an bind (boundObject) übergeben wird.

// Logs `false`:
console
.log(someFunction() === boundObject);
// Logs `true`:
console
.log(boundFunction() === boundObject);

Weitere Beispiele

Beim Aufrufen einer verknüpften Funktion kann der Wert von this nicht mit call oder apply geändert werden:

// Logs `true` - called `this` value is ignored:
console
.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console
.log(boundFunction.apply({foo: 'bar'}) === boundObject);

Beim Aufrufen einer verknüpften Funktion kann der Wert von this nicht geändert werden, indem die Funktion als Mitglied eines anderen Objekts aufgerufen wird:

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console
.log(obj.boundFunction() === boundObject);

Andernfalls, wenn this zum Zeitpunkt des Anrufs festgelegt ist:

function someFunction() {
 
return this;
}

const someObject = {hello: 'world'};

// Logs `true`:
console
.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console
.log(someFunction.apply(someObject) === someObject);

Der Wert von this ist das Objekt, das an call/apply übergeben wird.

Leider wird this durch Dinge wie DOM-Ereignis-Listener auf einen anderen Wert gesetzt. Die Verwendung kann zu schwer verständlichem Code führen:

Don'ts
element.addEventListener('click', function (event) {
 
// Logs `element`, since the DOM spec sets `this` to
 
// the element the handler is attached to.
  console
.log(this);
});

In solchen Fällen verwende ich this nicht, sondern:

Do
element.addEventListener('click', (event) => {
 
// Ideally, grab it from a parent scope:
  console
.log(element);
 
// But if you can't do that, get it from the event object:
  console
.log(event.currentTarget);
});

Andernfalls, wenn die Funktion über ein übergeordnetes Objekt (parent.func()) aufgerufen wird:

const obj = {
  someMethod
() {
   
return this;
 
},
};

// Logs `true`:
console
.log(obj.someMethod() === obj);

In diesem Fall wird die Funktion als Mitglied von obj aufgerufen. this ist also obj. Das geschieht zum Zeitpunkt des Aufrufs. Die Verknüpfung wird also getrennt, wenn die Funktion ohne übergeordnetes Objekt oder mit einem anderen übergeordneten Objekt aufgerufen wird:

const {someMethod} = obj;
// Logs `false`:
console
.log(someMethod() === obj);

const anotherObj = {someMethod};
// Logs `false`:
console
.log(anotherObj.someMethod() === obj);
// Logs `true`:
console
.log(anotherObj.someMethod() === anotherObj);

someMethod() === obj ist falsch, weil someMethod nicht als Mitglied von obj aufgerufen wird. Möglicherweise haben Sie dieses Problem schon einmal bei einem Versuch wie diesem festgestellt:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

Das funktioniert nicht, weil die Implementierung von querySelector seinen eigenen this-Wert betrachtet und davon ausgeht, dass es sich um eine Art DOM-Knoten handelt. Durch die oben genannte Änderung wird diese Verbindung unterbrochen. So gehen Sie vor:

const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);

Fun Fact: Nicht alle APIs verwenden this intern. Console-Methoden wie console.log wurden geändert, um this-Verweise zu vermeiden. log muss also nicht mehr an console gebunden werden.

Andernfalls, wenn die Funktion oder der übergeordnete Bereich sich im strengen Modus befindet:

function someFunction() {
 
'use strict';
 
return this;
}

// Logs `true`:
console
.log(someFunction() === undefined);

In diesem Fall ist der Wert von this nicht definiert. 'use strict' ist in der Funktion nicht erforderlich, wenn sich der übergeordnete Bereich im strengen Modus befindet (und alle Module sich im strengen Modus befinden).

Andernfalls:

function someFunction() {
 
return this;
}

// Logs `true`:
console
.log(someFunction() === globalThis);

In diesem Fall ist der Wert von this mit globalThis identisch.

Geschafft!

Webseite. Das ist alles, was ich über this weiß. Fragen? Habe ich etwas verpasst? Du kannst mir gern einen Tweet senden.

Vielen Dank an Mathias Bynens, Ingvar Stepanyan und Thomas Steiner für die Überprüfung.