Jedan od konfuznih pojmova u JavaScriptu je this
ključna riječ. Iako je već ispisano dosta tekstova upravo o ovom pojmu, ponekad je teško razumjeti i povezati sve činjenice koje su raspršene po internetu.
Jedan od izazova sa kojim se susretnemo u razumijevanju ove ključne riječi se javlja ako smo već upoznati sa nekim class-orijentisanim jezikom, kao što je Java na primjer. U takvim jezicima this
ključna riječ je vezana za instancu jedne klase, i korištenjem ove riječi referiramo se isključivo na tu instancu. Pomoću nje pristupamo poljima i metodama neke instance, dok to kod JavaScripta ponekad to i nije slučaj, pa nam to zna zadati glavobolju.
Primjer kako korištenje this
ključne riječi izgleda kod klasno baziranog jezika, pseudo kod:
class Klasa {
private String prop;
public Klasa(prop) {
this.prop = prop;
}
public String getProp() {
return this.prop;
}
}
var Obj = new Klasa("New Prop");
Obj.getProp()
S obzirom na to da od novijih verzija JavaScript jezika na raspolaganju imamo ključnu riječ class
koju možemo koristiti da mimikujemo klasno bazirano objektno orijentisano programiranje, ponekad se može činiti da je i JavaScript klasno orijentisan jezik.
Međutim, i dalje JavaScript i u novijim verzijama je "prototype" baziran OOP jezik. Pa imamo mogućnost da mimikujemo, ali ne i da se oslonimo kompletno na ponašanja klasno baziranog jezika, što ćemo najbolje vidjeti na primjeru this
ključne riječi.
Ovako izgleda skoro isti primjer prepisan u JavaScriptu sa jednom malom izmjenom:
class Klasa {
constructor(prop) {
this.prop = prop
}
toString() {
return function () {
this.prop
}
}
}
const Obj = new Klasa('random string')
Obj.toString()()
Ukoliko pokušate izvršiti kod iz primjera iznad, dobićete TypeError ... property of undefined
.
Razlog za to je što se vezivanje - referenciranje this ključne riječi u JavaScriptu dešava kao "runtime" vezivanje i kontekstualno je bazirano. Ovo u prijevodu znači da se this
određuje u zavisnosti od uslova kako i gdje je funkcija koja koristi ovu ključnu riječ pozvana.
Iz ovoga proističe da određivanje vrijednosti ove ključne riječi ne zavisi od toga na kome mjestu je napisana, kao što je slučaj u class-orijentisanim jezicima, pa se može činiti da ponekad se dešavaju nepredvidive stvari.
Ipak, postoji par pravila koja treba pratiti da bi ublažili glavobolje i nepredvidive situacije kada koristimo this
ključne riječi:
"Rule of thumb" određivanje thisa:
- ukoliko nije specificirano drugačije, po defaultu this će referencirati globalni objekat baziran na okruženju(browse = window);
- u strikt modu u globalnom kontekstu uvijek će biti
undefined
; važno je spomenuti da su moduli, klase, deklarisane i klasne ekspresije implicitno u strikt modu; - Ako je funkcija pozvana sa
call
,apply
ilibind
referenca nathis
će biti proslijeđeni objekat u funkciju. - Ako je funkcija pozvana korištenjem
new
operatora,this
referenca će biti na novi proizvedeni objekat. - I najviše neprijatnosti donosi
with
ekspresija koju u potpunosti treba izbjeći pa se nećemo ni osvrtati u tekstu. Slično je i saeval()
funkcijom koju nije preporučljivo koristiti i treba ju izbjeći ako je moguće.
Defaultno ponašanje (nije specificirano drugačije)
//globalni objekat
console.log(this)
// nije globalni objekat
(function(){
console.log(this)
})();
Iz primjera možemo vidjeti da this referenca nije vezana za instancu objekta, a isto tako nije vezana ni za mjesto u kom je napisana, nego je vezana za način na koji je pozvana.
Kako se dešava vezivanje
U JavaScriptu postoje 3 glavna izvršna konteksta za koji kada se pozovu kreiraju se izvršni rekordi. Ove rekorde, ili unose, možemo posmatrati kao objekte koji sadrže odredjene informacije. A to mogu biti:
- Function,
- Module
- Global.
Function rekord čuva veze o funkciji, pa tako i this
referencu, osim u slučaju kada je funkcija Arrow Funkcija(=>) o tom ćemo detaljnije kasnije u tekstu.
Module Rekord drži nepromjenjive veze izmedju ostalih rekorda i okruzenja, u biti čuva informacije o import {}, import default, export
.... I na kraju, global rekord se koristi kao najveći vanjski scope koji je dijeljen sa svim JavaScript elementima.
Kontekst prolazi kroz dvije faze a to su "Pravljenje" i "Izvršavanje" upravo tog Izvršnog konteksta. Prilikom faze "Pravljenja" kreiraju se rekordi vezani za taj kontekst kao što su npr. objekti gdje su pospremljene sve varijable"[VO]", scope lanac i closure, i ono što je nama trenutno bitno referenca na this objekat. Tako kad god se uspostavi novi Izvrsni kontekst dobijamo i novu this referencu.
Function Izvršni Kontekst > određivanja this ključne riječi u funkcijama
Ulazak u function izvršni kontekst se dešava prilikom pozivanja funkcije. Postoje 4 načina kako možemo pozvati funkciju I sve i jedan od sljedećih poziva će kreirati Function izvršni kontekst.
- Klasično pozivanje funkcije
func()
- Opcionalno vezivanje
obj?.property?.()
- Tagged templejti ("string interpolation")
functionstring ${expression} text``
- I funkcije evaluirane sa new operatorom
new Something()
Da bi znali koja je trenutna this referenca trebamo posmatrati trenutni izvršni kontekst, tacnije gdje je funkcija pozvana.
const Obj = {
toString() {
// Obj === this
return function () {
// Obj !== this
this.prop
}
}
}
const inner = Obj.toString()
inner()
Ako posmatramo kod možemo primjetiti da inner() funkcija nije pozivom vezana za toString funkciju niti za Obj objekat, nego je pozvana direktno u globalnom kontekstu pa možemo pretpostaviti da this referencira globalni objekat što bi značilo u strikt modu da ne referencira ništa da je vrijednost "undefined".
S druge strane, ukoliko bi kod izgledao ovako
const Obj = {
stuff: 'String'
toString() {
return this.stuff
}
}
Obj.toString()
Vidimo da je funkcija nije pozvana "sama" u globalnom kontekstu sa lijeve strane je Obj objekta pa možemo pretpostaviti da će referenca na this u ovom slučaju biti direktno povezana sa Obj objektom ukoliko se ne desi da neki drugi izvršni kontekst ne utiče.
Da ponovimo, svako od ovih će pozvati funkciju toString
i napraviti novi Function izvršni kontekst, te ako ništa ne mjenja to u tom izvršnom kontekstu referenca na this
će biti na objekat Obj
.
Obj.toString()
Obj.toString?.()
Obj?.toString()
Obj["toString"]()
Obj.toString``
Arrow funkcije => "fat arrow"
Iz sljedećeg primjera se može primjetiti da funkcija koja vraća funkciju iako se nalazi na objektu nema veze tim što živi na toj instanci neće referencirati njega nego Function kontekst funkcije toString, Pa smo u doba jQuery-a ovakve probleme riješavali na sljedeći način:
var Obj = {
stuff: 'String'
toString() {
var self = this
return function () {
return self.stuff
}
}
}
U toString metodi bi spremili trenutnu referencu na this i sa clousure-om bi imali dostupnu tu vrijednost i unutar funckije koju vraćamo iz toString()
funkcije. Pojavom es2015 verzije jezika dosta problema je riješeno i jezik je dobio novu dimenziju. Jedno od tih poboljšanja su i arrow funkcije (=>), koje su na jedan način otklonile i ovaj ponavljajuci kod var self = this
.
Kako je to postignuto? Same Arrow funkcije ne vezuju this referencu, vec ce prilikom izvrsavanja jedne Arrow funkcije u Function rekordu biti zabilježeno da je this
referenca leksična - preuzeta od roditeljskog konteksta, pa će engine rekurzivno pogledati sljedeće Izvršne rekorde dok ne pronađe this referencu. U ovom slučaju to će biti vanjska funkcija toString. Uz Arrow funkcije iz prošlog primjera će izgledati ovako
const Obj = {
stuff: 'String'
toString() {
return () => {
return this.stuff
}
}
}
Pa onda ako pozovemo ovu metodu na Obj.toString() trebalo bi da se ponaša onako kako smo i zamislili.
Ovo bi bilo implicitno vezivanje onako kako bi engine sam povezao this referencu u trenutnom kontekstu izvrsavanja.
Eksplicitno vezivanje
Ipak, treba spomenuti da postoji i explicitno vezivanje u kome je moguće "isforsirati" this referencu na onu koja nam odgovara ukoliko se desi da gubimo implicitne što je vrlo čest slučaj, pa nam jezik nudi funkcije kao sto su .call i .apply koje nam dozvoljavaju da sami promjenimo referencu.
Jezik nam je omogućio da sve naše napisane funkcije posjeduju i ove navedene funkcije koje se nalaze na Function.prototype.(call, apply, ...)
. Obe funkcije call i apply kao prvi parametar uzimaju objekat koji ćemo referencirati kao this
u toj funkciji i listu parametara koji treba da porslijedimo funkciji. Jedina razlika je da .call(thisRefObj, arg1, arg2)
prima "varargs" argument za pozivne arugmente funkciji koju pozivamo, dok apply
za argument očekuje niz argumenata .apply(thisRefObj, [arg1, arg2])
.
function toString(arg) {
return this.stuff + arg
}
const someObj = {
stuff: 'Random String'
};
toString.call(somObj, 'forwarded argument')
// varijanta sa apply
toString.apply(somObj, ['forwarded argument'])
Važno je pomenuti da u "sloppy" modu(kada se kod ne izvršava u strict modu) primitivne vrijednosti će biti coerceovane u objekte, tzv. primitive type boxing, pa ako proslijedimo .call(1, 'args')
1 će biti boxovan u Number(1)
tako i svi ostali primitivni tipovi dok undefined
ili null
koji mogu biti "opasni" jer će promjeniti this
na undefined/null
.
Dok u strict modu vrijednosti neće biti boxovane i bit će korišteni primitivi kao reference
"use strict"
function toString() {
console.log(this)
}
toString.call('random string')
ispisana vrijednost pozivanjem toString
bit će primitivna vrijednost "random string" za razliku od "slopy" moda gdje bi bila 'String { value }'
Kako ne bismo morali svaki puta pozivati funkciju sa nekom od call
ili apply
funkcijama, postoji i funkcija na prototipu .bind()
koja će zauvijek zaključati this referencu toj funkciji i vratit će novu funkciju sa tom this referencom.
function toString() {
return this.stuff
}
const bounded = toString.bind({ stuff: 'random string' })
bounded()
Povratna vrijednost će biti random string. Ovo isto mozemo postici i koristenjem call i apply.
Vrijednost u JavaScript klasama
Kada u JavaScriptu pozovemo funkciju koristeći new
operator, engine će interno postaviti odredjene propertije kao što su prototype chain, takodje, u slucaju da je napisan bazni konstruktor, postaviće this referencu na objekat koji je konstruisan. Pod baznim konstruktorom se podrazumijeva da funkcija ne extenda neku drugu konstruktorsku funkciju, npr. class something extends someSuperStuff
o čemu ćemo više kasnije.
Koristio sam class
u primjeru jer od ES 2015 verzije postoji i ključna riječ class u jeziku što je novi način da pišemo konstruktor funkcije.
Pominjem konstruktor funkcije iako više niko ne piše konstruktor funkcije, ipak ne treba zaboraviti da je class idalje samo sintatički dodatak konstruktor funkcijama, pa tako djelimo i dobre i loše strane konstruktor funkcija i sa novim klasama.
function FunctionConstructor(args){
this.stuff = args
}
var Obj = new FunctionConstructor('random string')
class ClassKeyword{
constructor(args){
this.stuff = args
}
}
const Obj = new ClassKeyword('random string');
Primjeri su identični i koristeći new
operator u pozadini se za nas automatski dešavanju 4 stvari a to su:
- Instancira se nova instanca -objekat- i vezuje se
this
na taj objekat u konstruktor funkciji. - Vezuje se
primjerObj.proto
naPrimjerKlasa.prototype
. - Vezuje se i
primjerObj.proto.constructor
zaPrimjerKlasa
. - Implicitno vraća
this
, koji se odnosi na instancu ->primjerObj
I važno napomenuti da su klase implicitno u strikt modu.
Ako se sjetimo funkcija sa početka gdje je toString()
vraćala funkciju vidjeli smo kako smo odvojili funkciju od objekta pa tako ako ste radili ili nažalost i sad radite react sa class komponentama call-back funckijama je trebalo eksplicitno referencirati this
jer bi se te funkcije pozivale u nekom od drugih izvršnih konteksta, a zbog ovog prvog pravila moguće je jednostavno eksplicitno postaviti this
na funkciju u konstruktoru pa bi to izgledalo ovako:
constructor(props) {
super(props)
this.handler = this.handler.bind(this)
}
Ako ne sačuvamothis
bi se desio gubitak reference i pokazivali bi naglobalThis
objekat, a to je (kako smo prethodno spomenuli) ustrict
modu uvijekundefined
a klase su implicitno ustrict
modu.
Jedno od ponašanja new ključne riječi kada klasa "nasljeđuje" druge klase class Derived extends Base {…}
, Naslijeđene klase ne postavljaju odmah this po pozivanju. To se dešava samo kada se dosegnu bazne klase kroz seriju super()
poziva (koji se dešavaju implicitno) zbog čega nije moguće koristiti this.stuff
prije poziva super()
u constructor funkciji. Pozivanjem super()
pozove se konstruktor bazne klase i postavi se this
` leksično u funkcijskom izvršnom rekordu.
class Base {
constructor(arg1) {
this.arg1 = arg1
}
}
class Derived extends Base {
constructor(arg1, arg2){
// Using `this` before `super` results in a ReferenceError.
super(arg1);
this.arg2 = arg2;
}
}
const derivedInstance = new Derived('Random', 'String');
derivedInstance
objekat će izgledati ovako:
Derived {arg1: 'Random', arg2: 'String'}
[[Prototype]]: Base
Od posljednje verzije ES2022 dobili smo i klasna polja koja se isto tako evaluiraju kada se klasa evaluira. Osnovni principi su da ako je static polje tada this referencira klasu, a ako polje nije static tada će referencirati instancu konkretan objekat.
Takođe smo dobili i private propertije koji se ponašaju isto kao i ostali propertiji, osim sto se izvršavaju u posebnom konekstu koji nije vidljiv ostalim konekstima.
TL;DR
Strikt mod se treba koristiti uvijek i svugdje. Bitno je razumjeti da ako pozivamo Obj.stuff()
this se referencira na Obj
baš kao i metoda u klasičnim class orijentisanim jezicima.
Ipak, ako tu metodu odvojimo u posebnu funkciju stuff()
koja nema objekat sa lijeve strane tada trebamo koristiti .bind
, .apply
ili .call
funckije da isforsiramo referencu na this koju želimo.
=> fat arrrow funkcije nemaju mogućnost da vezuju this
i koriste this
iz vanjskog lanca. Klase vezuju this
za instancu klase, osim ako se radi o static
nivou kada vezuju this na samu klasu.