Як зрозуміти ключове слово 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.