Kako razumjeti ključnu riječ this i kontekst u JavaScript-u

Kao što je spomenuto u jednom od mojih ranijih članaka, potpuno svladavanje JavaScript-a može biti dugo putovanje. Možda ste thisna svoje putovanje naišli kao programer za JavaScript. Kad sam započeo, prvi put sam ga vidio kada sam koristio eventListenersi s jQueryjem. Kasnije sam ga morao često koristiti s Reactom, a siguran sam da ste i vi to koristili. To ne znači da sam stvarno razumio što je to i kako u potpunosti preuzeti kontrolu nad njim.

Međutim, vrlo je korisno savladati koncept koji stoji iza toga, a kada se pristupi bistrog uma, to nije ni vrlo teško.

Kopajući u ovome

Objašnjenje thismože dovesti do puno zabune, već samo imenovanjem ključne riječi.

thisje usko povezan s vašim kontekstom u vašem programu. Krenimo skroz od vrha. Ako u naš preglednik samo upišete thiskonzolu, dobit ćete window-object, najudaljeniji kontekst za vaš JavaScript. Ako to učinimo u Node.js:

console.log(this)

završimo s {}praznim predmetom. Ovo je pomalo čudno, ali čini se da se Node.js tako ponaša. Ako to uradiš

(function() { console.log(this); })();

međutim, primit ćete globalobjekt, najudaljeniji kontekst. U tom kontekstu setTimeout, setIntervalsu pohranjeni. Slobodno se malo poigrajte s tim da biste vidjeli što možete učiniti s njim. Odavde gotovo da nema razlike između Node.js-a i preglednika. Koristit ću window. Samo zapamtite da će u Node.js to biti globalobjekt, ali to zapravo ne čini razliku.

Zapamtite: Kontekst ima smisla samo unutar funkcija

Zamislite da napišete program, a da ništa ne gnijezdite u funkcijama. Jednostavno biste napisali jedan redak za drugim, a da se ne spuštate niz određene strukture. To znači da ne morate pratiti gdje ste. Uvijek ste na istoj razini.

Kad započnete imati funkcije, možda ćete imati različite razine programa i thispredstavlja gdje se nalazite, koji se objekt naziva funkcijom.

Praćenje objekta pozivatelja

Pogledajmo sljedeći primjer i vidjet ćemo kako se thismijenja ovisno o kontekstu:

const coffee = { strong: true, info: function() { console.log(`The coffee is ${this.strong ? '' : 'not '}strong`) }, } coffee.info() // The coffee is strong

Budući da pozivamo funkciju koja je deklarirana unutar coffeeobjekta, naš se kontekst mijenja upravo u taj objekt. Sada možemo pristupiti svim svojstvima tog objekta this. U našem primjeru gore, također bismo ga mogli izravno uputiti radeći coffee.strong. Postaje zanimljivije, kada ne znamo u kojem kontekstu, u kojem smo objektu ili kad stvari jednostavno postanu malo složenije. Pogledajte sljedeći primjer:

const drinks = [ { name: 'Coffee', addictive: true, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, { name: 'Celery Juice', addictive: false, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, ] function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)] } pickRandom(drinks).info()

Predavanja i primjeri

Predavanja se mogu koristiti za apstrahiranje vašeg koda i dijeljenje ponašanja. Uvijek ponavljati infodeklaraciju funkcije u posljednjem primjeru nije dobro. Budući da su klase i njihove instance zapravo objekti, ponašaju se na isti način. Međutim, valja napomenuti da je deklariranje thisu konstruktoru zapravo predviđanje za budućnost, kada će postojati instanca.

Pogledajmo:

class Coffee { constructor(strong) { this.strong = !!strong } info() { console.log(`This coffee is ${this.strong ? '' : 'not '}strong`) } } const strongCoffee = new Coffee(true) const normalCoffee = new Coffee(false) strongCoffee.info() // This coffee is strong normalCoffee.info() // This coffee is not strong

Zamka: besprijekorno ugnježđeni pozivi funkcija

Ponekad završimo u kontekstu koji zapravo nismo očekivali. To se može dogoditi kada funkciju nesvjesno zovemo unutar konteksta drugog objekta. Vrlo čest primjer je kada koristite setTimeoutili setInterval:

// BAD EXAMPLE const coffee = { strong: true, amount: 120, drink: function() { setTimeout(function() { if (this.amount) this.amount -= 10 }, 10) }, } coffee.drink()

Što vi mislite coffee.amount?

...

..

.

Još uvijek je 120. Prvo, bili smo unutar coffeeobjekta, budući da je drinkmetoda deklarirana unutar njega. Upravo jesmo setTimeouti ništa drugo. To je točno to.

Kao što sam ranije objasnio, setTimeoutmetoda je zapravo deklarirana u windowobjektu. Kad ga zovemo, zapravo prebacujemo kontekst na windowponovno. To znači da su se naše upute zapravo pokušale promijeniti window.amount, ali na kraju nisu ifučinile ništa zbog- izjave. Da bismo se pobrinuli za to, moramo ispuniti bindsvoje funkcije (vidi dolje).

Reagirati

Koristeći React, nadamo se da će ovo uskoro biti prošlost, zahvaljujući Hooksu. Trenutno još uvijek moramo bindsve (o tome više kasnije) na ovaj ili onaj način. Kad sam započeo, nisam imao pojma zašto to radim, ali u ovom biste trenutku već trebali znati zašto je to potrebno.

Pogledajmo dvije jednostavne komponente React klase:

// BAD EXAMPLE import React from 'react' class Child extends React.Component { render() { return  Get some Coffee!  } } class Parent extends React.Component { constructor(props) { super(props) this.state = { coffeeCount: 0, } // change to turn into good example – normally we would do: // this._getCoffee = this._getCoffee.bind(this) } render() { return (    ) } _getCoffee() { this.setState({ coffeeCount: this.state.coffeeCount + 1, }) } }

Kada sada kliknemo na gumb koji je donio Child, primit ćemo pogrešku. Zašto? Budući da je React promijenio naš kontekst prilikom pozivanja _getCoffeemetode.

Pretpostavljam da React doista poziva metodu renderiranja naših komponenata u drugom kontekstu, kroz pomoćne klase ili slično (iako bih morao dublje kopati da bih to sa sigurnošću saznao). Stoga this.statenije definirano i pokušavamo pristupiti this.state.coffeeCount. Trebali biste primiti nešto poput Cannot read property coffeeCount of undefined.

Da biste riješili problem, morate bind(doći ćemo tamo) metode u našim razredima, čim ih prenesemo iz komponente gdje su definirane.

Pogledajmo još jedan generički primjer:

// BAD EXAMPLE class Viking { constructor(name) { this.name = name } prepareForBattle(increaseCount) { console.log(`I am ${this.name}! Let's go fighting!`) increaseCount() } } class Battle { constructor(vikings) { this.vikings = vikings this.preparedVikingsCount = 0 this.vikings.forEach(viking => { viking.prepareForBattle(this.increaseCount) }) } increaseCount() { this.preparedVikingsCount++ console.log(`${this.preparedVikingsCount} vikings are now ready to fight!`) } } const vikingOne = new Viking('Olaf') const vikingTwo = new Viking('Odin') new Battle([vikingOne, vikingTwo])

Prelazimo increaseCountiz jednog razreda u drugi. Kada increaseCountmetodu Vikingpozivamo, već smo promijenili kontekst i thiszapravo ukazujemo na Viking, što znači da naša increaseCountmetoda neće raditi kako se očekivalo.

Rješenje - veži

Najjednostavnije rješenje za nas bindsu metode koje će biti prenesene iz našeg izvornog objekta ili klase. Postoje različiti načini na koje možete povezati funkcije, ali najčešći je (također u Reactu) povezivanje u konstruktoru. Dakle, morali bismo dodati ovaj redak u Battlekonstruktor prije redaka 18:

this.increaseCount = this.increaseCount.bind(this)

Bilo koju funkciju možete vezati za bilo koji kontekst. To ne znači da funkciju uvijek morate vezati za kontekst u kojem je deklarirana (međutim, to je najčešći slučaj). Umjesto toga, možete ga povezati s drugim kontekstom. Uz bind, uvijek postavljate kontekst za deklaraciju funkcije . To znači da će svi pozivi za tu funkciju dobiti vezani kontekst kao this. Postoje još dva pomagača za postavljanje konteksta.

Funkcije strelice `() => {}` automatski vezuju funkciju za kontekst deklaracije

Prijavite se i nazovite

They both do basically the same thing, just that the syntax is different. For both, you pass the context as first argument. apply takes an array for the other arguments, with call you can just separate other arguments by comma. Now what do they do? Both of these methods set the context for one specific function call. When calling the function without call , the context is set to the default context (or even a bound context). Here is an example:

class Salad { constructor(type) { this.type = type } } function showType() { console.log(`The context's type is ${this.type}`) } const fruitSalad = new Salad('fruit') const greekSalad = new Salad('greek') showType.call(fruitSalad) // The context's type is fruit showType.call(greekSalad) // The context's type is greek showType() // The context's type is undefined

Can you guess what the context of the last showType() call is?

..

.

You’re right, it is the outermost scope, window . Therefore, type is undefined, there is no window.type

This is it, hopefully you now have a clear understanding on how to use this in JavaScript. Feel free to leave suggestions for the next article in the comments.

About the Author: Lukas Gisder-Dubé co-founded and led a startup as CTO for 1 1/2 years, building the tech team and architecture. After leaving the startup, he taught coding as Lead Instructor at Ironhack and is now building a Startup Agency & Consultancy in Berlin. Check out dube.io to learn more.