Как да разбера ключовата дума this и контекста в JavaScript

Както бе споменато в една от по-ранните ми статии, овладяването на JavaScript изцяло може да бъде дълго пътуване. Може да сте се натъквали thisна вашето пътуване като разработчик на JavaScript. Когато започнах, за първи път го видях при използване eventListenersи с jQuery. По-късно трябваше да го използвам често с React и съм сигурен, че и вие сте го използвали. Това не означава, че наистина разбрах какво е то и как да го контролирам напълно.

Много е полезно обаче да се овладее концепцията, която стои зад нея, а когато се подходи с ясен ум, също не е много трудно.

Ровейки се в това

Обяснението thisможе да доведе до много объркване, просто чрез назоваването на ключовата дума.

thisе тясно свързана с контекста, в който се намирате, във вашата програма. Нека започнем изцяло отгоре. В нашия браузър, ако просто въведете thisв конзолата, ще получите window-object, най-външния контекст за вашия JavaScript. В Node.js, ако го направим:

console.log(this)

завършваме с {}празен обект. Това е малко странно, но изглежда, че Node.js се държи по този начин. Ако го направиш

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

обаче ще получите globalобекта, най-външния контекст. В този контекст setTimeout, setIntervalсе съхраняват. Чувствайте се свободни да си поиграете малко с него, за да видите какво можете да направите с него. От тук почти няма разлика между Node.js и браузъра. Ще използвам window. Само не забравяйте, че в Node.js това ще бъде globalобектът, но всъщност няма значение.

Запомнете: Контекстът има смисъл само във функциите

Представете си, че пишете програма, без да влагате нищо във функциите. Можете просто да напишете един ред след друг, без да слизате по определени структури. Това означава, че не е нужно да следите къде се намирате. Винаги сте на едно ниво.

Когато започнете да имате функции, може да имате различни нива на вашата програма и да thisпредставя къде се намирате, какъв обект се нарича функция.

Проследяване на обекта на повикващия

Нека да разгледаме следния пример и да видим как се thisпроменят в зависимост от контекста:

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

Тъй като извикваме функция, която е декларирана вътре в coffeeобекта, нашият контекст се променя точно на този обект. Вече имаме достъп до всички свойства на този обект чрез this. В нашия пример по-горе бихме могли просто да го направим директно, като го правим coffee.strong. Става по-интересно, когато не знаем в какъв контекст, в какъв обект се намираме или когато нещата просто станат малко по-сложни. Погледнете следния пример:

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()

Класове и екземпляри

Класовете могат да се използват за абстрахиране на вашия код и споделяне на поведение. Винаги повтарянето на infoдекларацията за функция в последния пример не е добро. Тъй като класовете и техните екземпляри всъщност са обекти, те се държат по същия начин. Едно нещо, което трябва да се отбележи обаче е, че декларирането thisв конструктора всъщност е прогноза за бъдещето, когато ще има екземпляр.

Нека да разгледаме:

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

Ловушка: безпроблемно вложени функционални извиквания

Понякога се озоваваме в контекст, който всъщност не сме очаквали. Това може да се случи, когато несъзнателно извикаме функцията в контекста на друг обект. Много често срещан пример е при използване setTimeoutили setInterval:

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

Какво мислите, че coffee.amountе?

...

..

.

Все още е 120. Първо, бяхме вътре в coffeeобекта, тъй като drinkметодът е деклариран вътре в него. Просто направихме setTimeoutи нищо друго. Точно това е.

Както обясних по-рано, setTimeoutметодът всъщност е деклариран в windowобекта. Когато го извикваме, ние всъщност превключваме контекста към windowотново. Това означава, че нашите инструкции всъщност са се опитали да се променят window.amount, но в крайна сметка не са направили нищо поради if-заявлението. За да се погрижим за това, трябва да bindизпълним своите функции (вижте по-долу).

Реагирайте

Използвайки React, това се надяваме скоро да остане в миналото, благодарение на Hooks. В момента все още трябва да се справим с bindвсичко (повече за това по-късно) по един или друг начин. Когато започнах, нямах представа защо го правя, но в този момент вече трябва да знаете защо е необходимо.

Нека да разгледаме два прости компонента на React клас:

// 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, }) } }

Когато сега щракнем върху бутона, изобразен от Child, ще получим грешка. Защо? Тъй като React промени нашия контекст при извикване на _getCoffeeметода.

Предполагам, че React наистина извиква метода на рендериране на нашите Компоненти в друг контекст, чрез помощни класове или подобен (въпреки че би трябвало да се задълбоча, за да разбера със сигурност). Следователно, this.stateе недефинирано и ние се опитваме да осъществим достъп this.state.coffeeCount. Трябва да получите нещо подобно Cannot read property coffeeCount of undefined.

За да разрешите проблема, трябва bind(ще стигнем до там) методите в нашите класове, веднага щом ги предадем от компонента, където са дефинирани.

Нека да разгледаме още един общ пример:

// 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])

Предаваме increaseCountот един клас в друг. Когато извикаме increaseCountметода Viking, вече сме променили контекста и thisвсъщност сочим към Viking, което означава, че нашият increaseCountметод няма да работи както се очаква.

Решение - свързване

Най-простото решение за нас е bindметодите, които ще бъдат предадени от оригиналния ни обект или клас. Има различни начини, по които можете да свързвате функции, но най-често срещаният (също в React) е да го свържете в конструктора. Така че трябва да добавим този ред в Battleконструктора преди ред 18:

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

Можете да свържете всяка функция с всеки контекст. Това не означава, че винаги трябва да свързвате функцията с контекста, в който е декларирана (това е най-често срещаният случай). Вместо това можете да го свържете с друг контекст. С bind, вие винаги задавате контекста за декларация на функция . Това означава, че всички извиквания за тази функция ще получат обвързания контекст като this. Има двама други помощници за задаване на контекста.

Функции със стрелки `() => {}` автоматично свързват функцията към контекста на декларацията

Кандидатствайте и се обадете

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.