JavaScript: Was bedeutet das?

In JavaScript kann es schwierig sein, den Wert von this zu ermitteln. So gehts...

Archibald
Jake Archibald

Das this von JavaScript ist das Herzstück vieler Witze. Das liegt daran, dass es ziemlich kompliziert ist. Ich habe jedoch beobachtet, dass Entwickler wesentlich kompliziertere und domainspezifische Schritte ausführen, um sich mit diesem this auseinanderzusetzen. Wenn Sie sich bei this nicht sicher sind, hilft Ihnen dies hoffentlich weiter. Dies ist mein Leitfaden für this.

Ich beginne mit der spezifischsten Situation und schließe mit der am wenigsten spezifischen. Dieser Artikel ähnelt einem großen if (…) … else if () … else if (…) …, sodass Sie direkt zum ersten Abschnitt gehen können, der dem Code entspricht, den Sie sich gerade 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“ Wert für this 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 sich der Funktions- oder übergeordnete Bereich im strikten 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 nützlich, da der innere Wert von this nicht geändert werden kann und immer mit dem äußeren this identisch ist.

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 durch Aufrufen der Funktion als Mitglied eines anderen Objekts geändert werden:

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

Bei Pfeilfunktionen kann der Wert von this nicht durch Aufrufen der Funktion als Konstruktor geändert werden:

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

Gebundene Instanzmethoden

Wenn Sie bei Instanzmethoden sicherstellen möchten, dass this immer auf die Klasseninstanz verweist, verwenden Sie am besten Pfeilfunktionen und Klassenfelder:

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

Dieses Muster ist sehr nützlich, wenn Instanzmethoden als Event-Listener in Komponenten (wie React-Komponenten oder Webkomponenten) verwendet werden.

Der obige Code könnte so wirken, als würde die Regel „this wird das gleiche wie this im übergeordneten Bereich sein“ gebrochen. Es scheint aber sinnvoll zu sein, wenn Sie sich Klassenfelder als syntaktischen Zucker zum Festlegen von Elementen im Konstruktor vorstellen:

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 Patten bestehen darin, eine vorhandene Funktion im Konstruktor zu binden oder die Funktion im Konstruktor zuzuweisen. Wenn Sie aus irgendeinem Grund keine Klassenfelder verwenden können, ist das Zuweisen von Funktionen im Konstruktor eine sinnvolle Alternative:

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

Wenn die Funktion/Klasse mit new aufgerufen wird:

new Whatever();

Damit wird Whatever (oder die Konstruktorfunktion, wenn es sich um eine Klasse handelt) aufgerufen, wobei this auf das Ergebnis von Object.create(Whatever.prototype) gesetzt ist.

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 der Aufruf mit new erfolgt, 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();

Beim Aufruf mit new 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();

Wenn die Funktion einen „gebundenen“ this-Wert hat:

function someFunction() {
  return this;
}

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

Jedes Mal, wenn boundFunction aufgerufen wird, wird sein this-Wert an bind (boundObject) übergeben.

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

Weitere Beispiele

Wenn Sie eine gebundene Funktion aufrufen, 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 gebundenen Funktion kann der Wert von this nicht durch Aufrufen der Funktion als Element eines anderen Objekts geändert werden:

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

Wenn this zur Anrufzeit festgelegt ist, gilt Folgendes:

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 an call/apply übergebene Objekt.

Leider wird this durch Elemente wie DOM-Ereignis-Listener auf einen anderen Wert festgelegt, was zu einem schwer verständlichen Code führen kann:

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 Fällen wie den oben vermeide ich die Verwendung von this. Stattdessen:

Das sollten Sie tun:
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);
});

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, sodass this den Wert obj hat. Das passiert zum Zeitpunkt des Aufrufs, d. h. der Link wird unterbrochen, wenn die Funktion ohne ihr ü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 „false“, da someMethod nicht als Mitglied von obj aufgerufen wird. Vielleicht sind Sie auf dieses Problem gestoßen, als Sie versucht haben, so etwas zu tun:

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

Das funktioniert nicht, da die Implementierung von querySelector anhand des eigenen this-Werts davon ausgeht, dass es sich um einen Art DOM-Knoten handelt. Die Verbindung wird durch den oben angegebenen Knoten unterbrochen. Gehen Sie dazu so vor:

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

Übrigens: Nicht alle APIs verwenden intern this. Konsolenmethoden wie console.log wurden geändert, um this-Verweise zu vermeiden. Daher muss log nicht an console gebunden sein.

Wenn sich die Funktion oder der übergeordnete Bereich im strikten Modus befindet, gilt Folgendes:

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

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

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

Andernfalls:

function someFunction() {
  return this;
}

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

In diesem Fall entspricht der Wert von this dem Wert von globalThis.

Geschafft!

Webseite. Das ist alles, was ich über this weiß. Fragen? Irgendetwas, das ich übersehen habe? Sie können mir gerne einen Tweet senden.

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