Objavljeno: 13. April 2022

This u JavaScriptu

Tutorijali
Foto: Prateek Katyal / Unsplash

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 ili bind referenca na this ć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 sa eval() 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:

  1. Instancira se nova instanca -objekat- i vezuje se this na taj objekat u konstruktor funkciji.
  2. Vezuje se primjerObj.proto na PrimjerKlasa.prototype.
  3. Vezuje se i primjerObj.proto.constructor za PrimjerKlasa.
  4. 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čuvamo this bi se desio gubitak reference i pokazivali bi na globalThis objekat, a to je (kako smo prethodno spomenuli) u strict modu uvijek undefined a klase su implicitno u strict 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.

Top IT poslovi u tvom inboxu

Pretplati se na Dzobs.com newsletter i jednom sedmično ti šaljemo najnovije poslove za odabranu poziciju.

Zanimanje...