Стрілкові методи проти звичайних
Звичайний метод класу зберігається в прототипі й отримує this динамічно, у момент виклику, а стрілковий метод, це поле екземпляра, яке захоплює this під час створення об'єкта й більше його не втрачає. Різниця в синтаксисі мінімальна, але наслідки для пам'яті, наслідування та поведінки в колбеках радикально різні.
Теорія
TL;DR
- Звичайний метод лежить у
Class.prototypeі спільний для всіх екземплярів. - Стрілковий метод, це власна властивість екземпляра, нова функція на кожен
new. - У звичайного методу
thisвизначається в момент виклику, у стрілкового він зафіксований назавжди. - Стрілковий метод не втрачає контекст у
setTimeoutчиaddEventListener, звичайний втрачає безbind. superпрацює лише зі звичайними методами; у стрілковому полі його немає.- Стрілкові методи перелічувані й потрапляють у
Object.keys(), звичайні, ні.
Швидкий приклад
class User {
name = "Maria";
normal() {
console.log("normal:", this.name);
}
arrow = () => {
console.log("arrow:", this.name);
};
}
const u = new User();
setTimeout(u.normal, 500); // TypeError: Cannot read properties of undefined
setTimeout(u.arrow, 500); // arrow: Marianormal() втратив контекст, бо був викликаний як звичайна функція. arrow() контекст зберіг, бо стрілкова функція не має власного this, вона «захоплює» this з екземпляра під час ініціалізації поля.
Синтаксис
Звичайний метод:
class Button {
handleClick() {
console.log(this.label);
}
}Стрілковий метод (метод-поле):
class Button {
handleClick = () => {
console.log(this.label);
};
}На перший погляд відмінність лише в = () => {}, але поведінка кардинально різна.
Головна відмінність: поведінка this
| Тип методу | Як працює this |
|---|---|
| Звичайний метод | this визначається в момент виклику (динамічно) |
| Стрілковий метод | this жорстко прив'язаний до екземпляра при створенні об'єкта |
Саме тому у прикладі вище звичайний метод падає, а стрілковий друкує ім'я.
Де зберігаються методи
| Тип методу | Де зберігається | Спільний для всіх екземплярів? |
|---|---|---|
| Звичайний | У ClassName.prototype | Так |
| Стрілковий | У самому екземплярі (на кожному об'єкті) | Ні |
class A {
normal() {}
arrow = () => {};
}
const a1 = new A();
const a2 = new A();
console.log(a1.normal === a2.normal); // true, one method on the prototype
console.log(a1.arrow === a2.arrow); // false, each instance has its ownСтрілкові методи створюються заново при кожному new A(). Це дає незалежність this, але трохи гірше за пам'яттю та швидкістю створення об'єктів.
Обробники подій і колбеки
Звичайний метод втрачає this, якщо не прив'язати його вручну:
class Button {
constructor() {
this.label = "Click me";
document.body.addEventListener("click", this.handleClick);
}
handleClick() {
console.log(this.label); // undefined, the context is lost
}
}Рішення 1: прив'язати вручну.
document.body.addEventListener("click", this.handleClick.bind(this));Рішення 2 (сучасніше): використати стрілковий метод.
class Button {
label = "Click me";
handleClick = () => {
console.log(this.label); // "Click me"
};
}Стрілкові методи добре підходять для компонентів React і для обробників подій, де важливо, щоб this не губився.
Наслідування і перевизначення
Звичайні методи легко перевизначати в нащадках, зокрема через super:
class A {
greet() { console.log("Hello from A"); }
}
class B extends A {
greet() {
super.greet(); // works
console.log("Hello from B");
}
}А зі стрілковим полем так не вийде:
class A {
greet = () => console.log("Hello from A");
}
class B extends A {
greet = () => {
super.greet(); // error, there is no super method to call
};
}Причина в тому, що стрілкові методи створюються в екземплярі, а не в прототипі, тож перевизначення відбувається простим перезаписом поля, і батьківської версії в ланцюжку прототипів просто немає.
Відмінність при ітерації та налагодженні
Звичайні методи неперелічувані, тому їх не видно в Object.keys(). Стрілкові методи, це звичайні властивості екземпляра, тому вони перелічувані:
class Example {
method() {}
arrow = () => {};
}
const e = new Example();
console.log(Object.keys(e)); // ['arrow']Це також означає, що стрілкові поля копіюються через spread і Object.assign, а прототипні методи, ні.
Продуктивність
| Критерій | Звичайний метод | Стрілковий метод |
|---|---|---|
| Пам'ять | 1 функція на всіх | своя функція в кожного |
| Швидкість створення | Швидше | Повільніше |
Контекст this | Губиться без bind | Завжди збережений |
Підходить для super | Так | Ні |
| Підходить для колбеків | Потрібен bind | Підходить ідеально |
Підсумкове порівняння
| Особливість | Звичайний метод | Стрілковий метод |
|---|---|---|
| Де зберігається | Class.prototype | В екземплярі |
| Спільний для всіх | Так | Ні |
Поведінка this | Залежить від виклику | Зафіксована |
Втрата this у колбеках | Можлива | Немає |
Підтримка super | Так | Ні |
| Бере участь у наслідуванні | Так | Ні |
| Продуктивність | Краща | Трохи гірша |
| Використання в React і подіях | Незручно | Дуже зручно |
Висновок: використовуйте звичайні методи, якщо метод логічно спільний для всіх екземплярів і може бути перевизначений; використовуйте стрілкові, якщо важливо зберегти this, наприклад при передачі методу як колбека чи обробника події.
Типові помилки
- Робити всі методи стрілковими «про всяк випадок». Це множить функції в пам'яті й позбавляє клас нормального наслідування.
- Очікувати
superу стрілковому полі. Його там немає, бо поле не лежить у прототипі. - Дивуватися, що
Object.keys(instance)показує методи. Показує саме стрілкові поля, бо вони перелічувані власні властивості. - Вважати, що звичайний метод «сам» збереже
this. Безbindабо обгортки-стрілки він втратить контекст у будь-якому колбеку. - Плутати порядок ініціалізації. Поля-стрілки створюються при конструюванні екземпляра, тому в базовому класі вони ще недоступні на момент виконання його конструктора в нащадку.
- Використовувати стрілкові поля там, де метод треба мокати або підмінювати в тестах через прототип. Підміна
Class.prototype.methodна стрілкове поле не вплине.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.