Ключове слово this у JavaScript
this - це посилання на об'єкт, який зараз викликає функцію. Значення визначається в момент виклику, а не в момент оголошення.
Теорія
TL;DR
this- як дисплей "хто дзвонить": показує того, хто викликає функцію, а не саму функцію- Звичайні функції отримують
thisз місця виклику; стрілкові фіксують його зі scope, де були написані obj.method()встановлюєthisвobj; передача методу як callback губить прив'язку- Звичайні функції для методів об'єкта, стрілкові для callback і вкладених функцій
call,apply,bindдозволяють встановитиthisвручну
Швидкий приклад
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-утиліт.
Типові помилки
Стрілкова функція як метод об'єкта:
const user = {
name: 'Bob',
greet: () => console.log(this.name) // помилка: стрілка не має прив'язки до user
};
user.greet(); // undefinedСтрілкові функції не мають доступу до об'єкта, в якому вони оголошені. Для будь-якого методу, що читає властивості свого об'єкта, потрібна звичайна функція.
Втрата this у callback:
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, де посилання на метод потрапляє до обробника черги і падає без очевидної помилки.
Неприв'язаний обробник події:
function Counter() {
this.count = 0;
document.querySelector('button').addEventListener('click', function() {
this.count++; // this = елемент button, не екземпляр Counter
});
}Виправлення: стрілкова функція або .bind(this) при додаванні слухача.
new зі стрілковою функцією:
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 і стабільні форми об'єктів покращують продуктивність.
Приклади
Виклик методу проти відірваного виклику
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
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
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 та асинхронному коді з чергами завдань.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.