Poznajemy słówko kluczowe this i jeśli nie wszystkie, to większość różnych pułapek i trudnych bądź mylących spraw związanych z tym słówkiem i jego użyciem, na jakie możemy napotkać się w JavaScript.

This w normalnych metodach – funkcje zwykłe i strzałkowe

Słówko „this” w metodach obiektów wskazuje na właściwości tych obiektów – pod warunkiem, że używamy „normalnych” funkcji:

let person = {
  name: "John",
  last_name: "Doe",
  fullname() {
    return this.name + " " + this.last_name;
  }
}
console.log(person.fullname());
//John Doe

Funkcja strzałkowa nie zadziała poprawnie – będzie undefined:

let person = {
name: "John",
last_name: "Doe",
fullname: () => {return this.name + " " + this.last_name;}
}

console.log(person.fullname());
// undefined

This, setTimoeut i funkcja strzałkowa oraz zwykła – uwaga

W setTimeout jest dokładnie odwrotnie i fakt, że funkcja niestrzałkowa tworzy własny kontekst może być zgubne:

let person = {
 name: "John",
 last_name: "Doe",
 fullname() {
         setTimeout(function(){
         console.log(this.name + " " + this.last_name);
         }, 1000);
    }
}
person.fullname();
//undefined

Chodzi rzecz jasna o funkcję callback przekazaną do setTimeout, metoda MUSI być niestrzałkowa albo nie mamy dostępu do this. Żeby jednak callback przekazany do setTimeout miał dostęp do this metody, sam callback musi być strzałkowy.

let person = {
  name: "John",
  last_name: "Doe",
  fullname() {
     setTimeout(() => console.log(this.name + " " + this.last_name), 1000);
   }
}
person.fullname();
//John Doe

Metoda fullname jest niestrzałkowa, tradycyjna, a zatem ma dostęp w this do swojego kontekstu. Callback w setTimeout jest strzałkowy, więc jego this nie wskazuje na obiekt window (z którego setTimeout pochodzi) tylko poziom wyżej, na metodę fullname, która ma poprawnie ustawiony this.

This-that pattern – oldschoolowy sposób radzenia sobie

Rzućmy okiem na ten kod jeszcze raz, mając na uwadze, że funkcja tradycyjna tworzy kontekst to this ze swojego otoczenia:

let person = {
 name: "John",
 last_name: "Doe",
 fullname() {
         setTimeout(function(){
         console.log(this.name + " " + this.last_name);
         }, 1000);
   }
}
person.fullname();
//undefined

Metoda fullname bierze this ze swojego otoczenia, czyli obiektu person i ma dostęp pod this do jego właściwości. Tak samo klasyczna funkcja wewnątrz setTimeout bierze kontekst pod this ze swojego otoczenia, czyli z obiektu window, z którego setTimeout pochodzi.

Jeszcze raz:

let person = {
  name: "John",
  last_name: "Doe",
  fullname() {
      console.log(this.name + " " + this.last_name);
      //tutaj jeszcze mamy połączenie z obiektem
            setTimeout(function(){
                //tu już this wskazuje na window
                console.log(this.name + " " + this.last_name);
                }, 1000);
            }
}

person.fullname();
//John Doe
//(po 1 sek)  undefined

Możemy zatem zastosować this-that pattern:

let person = {
 name: "John",
 last_name: "Doe",
 fullname() {
    const that = this;
               setTimeout(function(){
               console.log(that.name + " " + that.last_name);
               }, 1000);
           }
}
person.fullname();
//John Doe

Bind – zamiast this-that oraz re-używanie metod na innym obiekcie

Bind możemy używać zamiast wzorca this-that, choć ja osobiście zawsze używam this-that wewnątrz jednego obiektu, zaś bind traktuję, jako coś, co pozwala tylko i wyłącznie pożyczyć z jednego obiektu funkcję ale przypisać ją do kontekstu drugiej:

let person = {
name: "John",
last_name: "Doe",
fullname() {
           const that = this;
           setTimeout(function(){
                   console.log(that.name + " " + that.last_name);
                      }, 1000);
           }
}

person.fullname();
//John Doe

let person2 = {
              name: "Jane",
              last_name: "Doe"
}

const person2fullname = person.fullname.bind(person2);
person2fullname();
//Jane Doe

Do jednorazowego wywołania funkcji możemy z jednego obiektu na innym obiekcie możemy użyć call albo apply:

let person = {
            name: "John",
            last_name: "Doe",
            fullname() {
            const that = this;
            setTimeout(function(){
                   console.log(that.name + " " + that.last_name);
                   }, 1000);
           }
}

person.fullname();
//John Doe

let person2 = {
              name: "Jane",
              last_name: "Doe"
}

person.fullname.call(person2)
person.fullname.apply(person2)
//Jane Doe
//Jane Doe

Różnica jest taka, że gdyby nasza funkcja fullname przyjmowała jakieś argumenty, to w call przekazujemy je „normalnie” zaś w apply w postaci tablicy z argumentami.

Funkcja na poziomie globalnym – this ma kontekst window

Funkcja na poziomie globalnym wskazuje na obiekt window:

<script>
function logThis(){
    console.log(this);
}
logThis();
//window
</script>

Jeszcze raz, dla zobrazowania:

<script>
 var x = 11;
 window.y = 12;
 function logThis(){
    console.log(this.x, this.y);
 }
 logThis();
 //11 12
 </script>

Callbacki dla funkcji window takich jak setTimeout czy setInterval też będą wskazywać na window, chyba że użyjemy strzałkowej a setTimeout znajduje się w normalnej (niestrzałkowej) metodzie obiektu – wtedy na ten obiekt, to już ustaliliśmy.

Funkcje konstruktory – ciekawy przypadek

Funkcje-konstruktory używają słówka kluczowego this oraz słówka kluczowego new do utworzenia nowego obiektu. Tak to działa:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
let person = new Person("John", 30);
console.log(person.name, person.age);
//John 30

Musimy pamiętać, że bez this nic nie osiągniemy:

 function Person(name, age) {
            this.name = name;
            this.age = age;
            const age_in_months = this.age * 12;
}
let person = new Person("John", 30);
console.log(person.age, person.age_in_months);
//30 undefined

Prawidłowe przypisanie pola odbywa się poprzez this:

function Person(name, age) {
      this.name = name;
      this.age = age;
      this.age_in_months = this.age * 12;
}
let person = new Person("John", 30);
console.log(person.age, person.age_in_months);
//30 360

Dzisiaj istnieje sposób, aby w przypadku gdy ktoś zapomni słówka kluczowego new mimo wszystko alokować pamięć dla obiektu przekazując odpowiednie argumenty i taki obiekt zwrócić:

function Person(name, age) {
   if (!new.target) 
         return new Person(name, age);
   this.name = name;
   this.age = age;
   this.age_in_months = this.age * 12;
}
let person = Person("John", 30);
console.log(person.age, person.age_in_months);
//30 360

Oczywiście co pod tym if-em damy to już nasza sprawa. Ja tutaj zwracam wersję Person z new i przekazuję argumenty, ale nic nie stoi na przeszkodzie, aby funkcja-konstruktor zawołana z new tworzyła nowy obiekt, zaś bez new działała w jakiś inny sposób.