Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

🔥 Zgarnij PŁATNY STAŻ w 3 edycji kursu programowania front end (zapisy do 22.01) 🔥

Dlaczego this nie działa?

Wyjaśnienie dla etapu nauki podstaw JavaScriptu

Słowo this pozwala nam odwołać się do danego obiektu – czyli np. wykorzystać jego wartości lub wywołać konkretną metodę. Potrafi jednak sprawiać pewne problemy. Możemy sobie z nimi łatwo poradzić, jeśli zrozumiemy, co dzieje się „pod spodem”. Zobacz trzy przykłady problemów z this. Artykuł przeznaczony jest dla osób uczących się podstaw JavaScriptu.

Spis treści

 Metoda korzystająca z this zapisana do nowej zmiennej

Po co zapisywać metodę obiektu do osobnej zmiennej? Chociażby dla naszej wygody i zwiększenia czytelności kodu (to nie musi być jedyny powód, ale nie chcę komplikować tłumaczenia).

Załóżmy, że wybór metody zależy od użytkownika – jak w kalkulatorze, w którym wybiera się działanie. Warto zabezpieczyć nasz kod przed błędami (w końcu użytkownik nie zawsze robi to, czego od niego oczekujemy).

Pokażę to na przykładzie konstruktora, czyli funkcji na bazie której możemy tworzyć nowe obiekty (przykład klasy będzie niżej) – być może na razie zetknąłeś się właśnie z nim (był powszechnie wykorzystywany przed wejściem klas w JS6).

function Calculator() {
  this.actions = {
    '+': this.add,
    '-': this.subtract
  }
  this.history = []
}

Calculator.prototype.add = function(number1, number2) {
  const result = number1 + number2
  this.history.push(`${number1} + ${number2} = ${result}`)
  return result

}

Calculator.prototype.subtract = function(number1, number2) {
  const result = number1 - number2
  this.history.push(`${number1} - ${number2} = ${result}`)
  return result
}

const calc = new Calculator()
const operation = prompt("Wybierz operację: + lub -")


if (typeof calc.actions[operation] === "function") {
  const num1 = prompt("Podaj pierwszą liczbę")
  const num2 = prompt("Podaj drugą liczbę")
  console.log(calc.actions[operation](+num1, +num2)) // powtarzam ten sam kod co w warunku (znak + przy parametrach to tzw. unary plus – zamienia wartość na typ number)
} else {
  alert("Podaj prawidłową operację")
}

W takiej sytuacji możesz chcieć przypisać wybraną przez użytkownika metodę do osobnej zmiennej:

const action = calc.actions[operation]

Po uruchomieniu kalkulatora otrzymasz jednak błąd: Uncaught TypeError: this.history.push is not a function

Dzieje się tak dlatego, że przypisanie metody do zmiennej czyni z niej nową funkcję z nowym kontekstem. Z tego powodu this wskazuje już nie na obiekt, z którego pochodzi metoda, lecz na obiekt globalny (lub w zależności od konfiguracji środowiska – na undefined). Obiekt globalny natomiast nie ma właściwości history, na której moglibyśmy wykonać jakiekolwiek operacje (np. push). To powoduje błąd.

Wystarczy jednak skorzystać z metody .bind() – w pierwszym parametrze przyjmuje ona obiekt, na który ma wskazywać słówko this:

function Calculator() {
  this.actions = {
    '+': this.add,
    '-': this.subtract
  }
  this.history = []
}

Calculator.prototype.add = function(number1, number2) {
  const result = number1 + number2
  this.history.push(`${number1} + ${number2} = ${result}`)
  return result

}

Calculator.prototype.subtract = function(number1, number2) {
  const result = number1 - number2
  this.history.push(`${number1} - ${number2} = ${result}`)
  return result
}

const calc = new Calculator()
const operation = prompt("Wybierz operację: + lub -")
const action = calc.actions[operation].bind(calc)

if (typeof action === "function") {
  const num1 = prompt("Podaj pierwszą liczbę")
  const num2 = prompt("Podaj drugą liczbę")
  console.log(action(+num1, +num2)) // już nie powielam kodu i mój zapis jest czytelniejszy (znak + przy parametrach to tzw. unary plus – zamienia wartość na typ number)
} else {
  alert("Podaj prawidłową operację")
}

 Metoda .super() w konstruktorze klasy JavaScript

Jeżeli korzystamy z dziedziczenia, możemy przypadkowo napotkać błąd: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

A wystarczyło nie pomylić linijek:

class Person {
  constructor(surname, age) {
    this.surname = surname;
    this.age = age;
  }
}

class Student extends Person {
  constructor(surname, age, grade) {
    this.grade = grade;
    super(surname, age); // błąd! Metodę .super() należy wywołać na początku
  }
}

let kowalsky = new Student('Kowalsky', 18, 'A');
console.log(kowalsky.grade)

Dlaczego tak się dzieje? Nie ma to bezpośredniego związku z this (choć informacja o nim pojawia się w błędzie) – po prostu metoda .super() wywołuje konstruktor klasy bazowej (rodzica), który inicjuje jej właściwości i metody (jest to też związane ze sposobem implementacji dziedziczenia w JS, lecz to temat na osobny artykuł). Gdyby nie ostrzegający nas błąd, to nadpisalibyśmy sobie wszystkie wcześniejsze pola klasy potomnej (w tym przypadku to właściwość .grade()), a potem zastanawiali się, czemu niektóre właściwości i metody nie działają.

Prawidłowy zapis to:

class Student extends Person {
  constructor(surname, age, grade) {
    super(surname, age); // metodę .super() wywołuję na początku instrukcji konstruktora
    this.grade = grade;
  }
}

 Funkcja zwykła i funkcja strzałkowa jako callback w metodzie obiektu

Jeśli zwykła funkcja (czyli ta deklarowana za pomocą słowa function) nie zostanie wywołana jako metoda obiektu, this będzie wskazywać na obiekt globalny (lub zależnie od konfiguracji środowiska: na undefined), a jeśli będzie wywołana na obiekcie jako metoda – this będzie wskazywać na ten obiekt, czyli na to, co jest „po lewej stronie kropki”.

Widać to na poniższym przykładzie (potraktuj go jako wprowadzenie do kolejnego przykładu, nie jako dobrą praktykę programowania):

class Person {
  constructor(surname, age) {
    this.surname = surname;
    this.age = age;
  }
  greet() {
    console.log(`${this.surname} wants to say hello. Get ready!`)

    function sayHello() {
      console.log(`Hi! My surname is ${this.surname} and I am ${this.age} years old.`);
    };
    
    sayHello()
  }
}

let kowalsky = new Person('Kowalsky', 18);
kowalsky.greet();
// linia 7: "Kowalsky wants to say hello. Get ready!"
// linia 10: Uncaught TypeError: Cannot read properties of undefined (reading 'surname')

W metodzie greet() słówko this wskazuje na obiektu kowalsky (który jest „po lewej stronie kropki”), dlatego imię "Kowalsky" wyświetla się w konsoli. W drugim przypadku funkcja sayHello() nie korzysta już z kontekstu tego obiektu, ponieważ stworzyła własny kontekst („z lewej strony kropki” nic nie ma) – dlatego otrzymujemy błąd (obiekt globalny window, na który teraz wskazuje this, nie ma właściwości surname, którą moglibyśmy odczytać).

Przejdźmy do bardziej życiowego przykładu: callbacku w metodzie obiektu.

class Person {
  constructor(surname, age) {
    this.surname = surname;
    this.age = age;
  }
  greet() {
    setTimeout(function() {
      console.log(`Hi! My surname is ${this.surname} and I am ${this.age} years old.`);
    }, 1000);
  }
}

Działa to na tej samej zasadzie: deklaracja zwykłej funkcji w callbacku (tym razem jest to funkcja bez nazwy, czyli anonimowa) stworzyła nowy kontekst i this w niej użyte nie wskazuje już na obiekt kowalsky.

Z pomocą przychodzi tu funkcja strzałkowa, która nie tworzy własnego kontekstu – korzysta więc z tego, w którym została wywołana (czyli z tego, „co jest po lewej stronie kropki”):

class Person {
  constructor(surname, age) {
    this.surname = surname;
    this.age = age;
  }
  greet() {
    setTimeout(() => {
      console.log(`Hi! My surname is ${this.surname} and I am ${this.age} years old.`);
    }, 1000);
  }
}

let kowalsky = new Person('Kowalsky', 18);
kowalsky.greet(); // "Hi! My surname is Kowalsky and I am 18 years old."

 

Na tym etapie nauki tyle wystarczy. Temat this na pewno do Ciebie wróci, jeśli np. zechcesz uczyć się programowania obiektowego lub zaczniesz korzystać z Reacta. Nie szkodzi, jeśli nie wszystko teraz rozumiesz – z czasem obeznasz się z obiektami, klasami i metodami, więc niektóre rzeczy samoistnie staną się jasne. Powodzenia w nauce!

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.

Mam coś dla Ciebie!

W każdy piątek rozsyłam motywujący do nauki programowania newsletter!

Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.

PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.