Skip to main content

Ключове слово this у JavaScript

this - це посилання на об'єкт, який зараз викликає функцію. Значення визначається в момент виклику, а не в момент оголошення.

Теорія

TL;DR

  • this - як дисплей "хто дзвонить": показує того, хто викликає функцію, а не саму функцію
  • Звичайні функції отримують this з місця виклику; стрілкові фіксують його зі scope, де були написані
  • obj.method() встановлює this в obj; передача методу як callback губить прив'язку
  • Звичайні функції для методів об'єкта, стрілкові для callback і вкладених функцій
  • call, apply, bind дозволяють встановити this вручну

Швидкий приклад

javascript
const obj = { name: 'Alice', regular: function() { console.log(this.name); }, // this = obj arrow: () => console.log(this.name) // this = зовнішній scope }; obj.regular(); // "Alice" obj.arrow(); // undefined (strict mode)

Дві функції, один об'єкт, протилежні результати. Виклик obj.regular() прив'язує this до obj. Стрілкова функція ігнорує це повністю. Вона зафіксувала this зовнішнього scope в момент створення, і нічого це не змінить.

Головна різниця

Звичайні функції визначають this у момент виклику. obj.method() - this є obj; fn() сама по собі - this є глобальний об'єкт або undefined у strict mode. Стрілкові функції повністю пропускають цей механізм. Вони фіксують this батьківського scope в момент створення, і це неможливо змінити. Виклики call, apply або bind не мають жодного ефекту на this стрілкової функції.

Коли що використовувати

  • Метод об'єкта, який читає або змінює дані екземпляра: звичайна функція
  • Callback для setTimeout, forEach або обробників подій: стрілкова, зберігає зовнішній this
  • Конструктор з new: тільки звичайна функція. Стрілкові кидають TypeError: not a constructor
  • Обробник подій у класі: arrow class field (handleClick = () => {}) позбуває від ручного bind у конструкторі
  • Утилітна функція без об'єктного контексту: стрілкова, немає ризику випадково звернутись до глобального this

Як this визначається під капотом

JavaScript-рушій прикріплює execution context до кожного виклику функції. Для звичайних функцій рушій перевіряє місце виклику: obj.fn() встановлює this як obj; fn() без об'єкта повертається до глобального (або undefined у strict mode). bind() повертає нову функцію з жорстко закріпленим this, який не можна перевизначити при виклику. Стрілкові функції взагалі не беруть участі в цьому механізмі. Вони захоплюють this батьківського scope в момент створення.

Одна деталь середовища: у ESM-модулях глобальний this завжди undefined. У CommonJS він вказує на module.exports. Це варто пам'ятати при відладці Node.js-утиліт.

Типові помилки

Стрілкова функція як метод об'єкта:

javascript
const user = { name: 'Bob', greet: () => console.log(this.name) // помилка: стрілка не має прив'язки до user }; user.greet(); // undefined

Стрілкові функції не мають доступу до об'єкта, в якому вони оголошені. Для будь-якого методу, що читає властивості свого об'єкта, потрібна звичайна функція.

Втрата this у callback:

javascript
const logger = { prefix: '[INFO]', log: function() { console.log(this.prefix); // undefined, this = global у setTimeout } }; setTimeout(logger.log, 0); // відірвано від об'єкта setTimeout(() => logger.log(), 0); // працює: стрілка викликає метод через об'єкт

Передача logger.log у setTimeout витягує функцію з об'єкта - прив'язка губиться. Стрілка-обгортка виправляє це, бо викликає метод безпосередньо на об'єкті, а не передає голе посилання. Це найчастіша помилка з this, яку я бачу на code review, зокрема у production-коді Express, де посилання на метод потрапляє до обробника черги і падає без очевидної помилки.

Неприв'язаний обробник події:

javascript
function Counter() { this.count = 0; document.querySelector('button').addEventListener('click', function() { this.count++; // this = елемент button, не екземпляр Counter }); }

Виправлення: стрілкова функція або .bind(this) при додаванні слухача.

new зі стрілковою функцією:

javascript
const Ctor = () => {}; new Ctor(); // TypeError: Ctor is not a constructor

У стрілкових функцій немає внутрішнього слоту [[Construct]]. Конструктором може бути тільки звичайна функція.

Де зустрічається в реальному коді

  • React class components: this.setState() і this.props залежать від того, щоб this вказував на екземпляр компонента. Arrow class fields (handleClick = () => {}) замінили ручний bind у конструкторах і досі часто зустрічаються в legacy-коді
  • Express: обробники помилок у класах потребують bind або стрілкових методів, щоб this залишався на екземплярі
  • Node.js Transform streams: this.push(chunk) усередині _transform працює тому, що Node викликає метод на екземплярі потоку
  • Lodash: _.bind(fn, context) і _.bindAll(obj, methods) існують саме для виправлення this у коді з великою кількістю callback-ів

Питання на співбесіді

Q: Що таке this всередині setTimeout(fn, 0)?
A: Глобальний об'єкт, або undefined у strict mode. setTimeout викликає функцію без об'єктного контексту, тому прив'язка губиться.

Q: В чому різниця між call, apply і bind?
A: Усі три встановлюють this явно. call і apply викликають функцію одразу: call приймає аргументи по одному, apply - масивом. bind повертає нову функцію з постійно встановленим this, без виклику.

Q: Що таке this на верхньому рівні ESM-модуля?
A: Завжди undefined. У CommonJS-модулях це module.exports.

Q: Як new впливає на this?
A: new створює порожній об'єкт, встановлює this на нього всередині конструктора і повертає цей об'єкт, якщо конструктор не повернув інший об'єкт явно.

Q: Чи можна змінити this стрілкової функції через call або bind?
A: Ні. Стрілкові функції ігнорують будь-який this, переданий через call, apply або bind. Лексична прив'язка постійна з моменту створення функції.

Q: (Senior) Як V8 оптимізує код з this?
A: V8 використовує inline caches (IC) для відстеження типу this на кожному місці виклику. Якщо тип стабільний між викликами (monomorphic), TurboFan агресивно оптимізує виклик. Якщо тип змінюється (polymorphic), V8 деоптимізує. Саме тому strict mode і стабільні форми об'єктів покращують продуктивність.

Приклади

Виклик методу проти відірваного виклику

javascript
const car = { speed: 60, accelerate: function() { this.speed += 10; console.log(this.speed); } }; car.accelerate(); // 70, this = car const fn = car.accelerate; fn(); // NaN у strict mode (undefined + 10) або змінює глобальну speed

Присвоєння методу у змінну відриває його від об'єкта. Тіло функції однакове в обох випадках. Різниться лише місце виклику, і саме воно визначає this.

React клас з bind і arrow class field

javascript
class UserProfile extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); // ручний bind для звичайного методу } handleClick() { this.setState({ count: this.state.count + 1 }); // this = екземпляр через bind } handleArrow = () => { this.setState({ count: this.state.count + 1 }); // this = екземпляр автоматично }; render() { return <button onClick={this.handleArrow}>{this.state.count}</button>; } }

Обидва підходи працюють. Arrow class field виглядає чистіше і не потребує конструктора. Прив'язаний через bind метод ділиться через прототип між екземплярами. На практиці більшість команд обирають стрілкові поля - різниця в пам'яті для UI-компонентів мізерна.

Втрата this в Express middleware

javascript
app.use((req, res, next) => { const logger = { userAgent: req.get('User-Agent'), log: function() { console.log(this.userAgent); // undefined, this = global у setTimeout } }; setTimeout(logger.log, 0); // не працює setTimeout(() => logger.log(), 0); // працює: стрілка викликає метод через об'єкт next(); });

Стрілка-обгортка () => logger.log() не передає посилання на функцію - вона викликає метод безпосередньо на об'єкті, зберігаючи прив'язку. Цей патерн часто зустрічається у middleware та асинхронному коді з чергами завдань.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?