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.