Статичні методи в JavaScript
Статичні методи в JavaScript - це функції, прикріплені безпосередньо до конструктора класу, а не до екземплярів чи прототипу. Їх викликають через ім'я класу, без new.
Теорія
TL;DR
- Аналогія: як
Math.max()- викликаєшMath, а не екземпляр Math НазваКласу.метод()працює;екземпляр.метод()кидає TypeError- Статичні методи зберігаються на конструкторі класу, минаючи ланцюжок прототипів
- Використовуй, коли логіці не потрібен
thisекземпляра - Правило вибору: немає стану екземпляра - роби статичним
Швидкий приклад
class Calculator {
static add(a, b) { // Прикріплено до Calculator, не до прототипу
return a + b;
}
}
console.log(Calculator.add(3, 4)); // 7
const calc = new Calculator();
calc.add(3, 4); // TypeError: calc.add is not a functionМетод add не торкається даних екземпляра, тому належить класу. Пошук через екземпляр провалюється, бо статичні методи взагалі не потрапляють у прототип.
Ключова різниця
Методи екземплярів зберігаються на Class.prototype і доступні кожному об'єкту, створеному через new. Статичні методи зберігаються безпосередньо на функції-конструкторі. Тому calc.__proto__ не має властивості add, звідси й TypeError. Це не баг і не дивна поведінка - так і задумано специфікацією.
Коли використовувати
- Математичні або рядкові утиліти без стану:
MathUtils.sum(a, b),StringUtils.slugify(str) - Фабричні методи (factory methods), що створюють екземпляри з сирих даних:
Person.fromString('Alice, 30') - Загальні хелпери, які зручно згрупувати під ім'ям класу, а не розкидати як вільні функції
- Валідатори, що повторно використовуються в різних маршрутах:
RouteValidator.validateUserId(id)в Express
Як це працює всередині
V8 зберігає статичні методи як звичайні властивості на функції-конструкторі під час обробки класу, а не на Class.prototype. Коли ти викликаєш MyClass.staticMethod(), рушій виконує прямий пошук властивості на конструкторі й прив'язує this до самого класу. Жодного обходу ланцюжка прототипів, жодного виділення пам'яті під екземпляр.
Поширені помилки
Виклик статичного методу через екземпляр
const utils = new MathUtils();
utils.sum(1, 2); // TypeError: utils.sum is not a functionСтатичні методи знаходяться на класі, а не на Class.prototype. Екземпляр не має до них жодного шляху. Рішення: MathUtils.sum(1, 2).
Очікування, що this всередині статика - це екземпляр
class Example {
static whoami() { console.log(this); }
}
Example.whoami(); // Виводить клас Example, а не undefinedВсередині статичного методу this - це конструктор класу, а не екземпляр і не undefined. Розробники, що прийшли з Java або Python, часто очікують null. Краще взагалі не покладатися на this у статичних методах і передавати дані параметрами.
Мутабельний спільний стан через статичні властивості
class Counter {
static count = 0;
static increment() { this.count++; }
}
Counter.increment();
Counter.increment();
// Counter.count тепер 2, глобально для всіхЦей патерн ламає тести частіше, ніж здається - особливо коли тести запускаються паралельно й використовують один клас. Для ізольованого стану використовуй властивості екземпляра або closure.
Спадкування без super
class Animal {
static getSpecies() { return 'Animal'; }
}
class Dog extends Animal {}
console.log(Dog.getSpecies()); // 'Animal', а не 'Dog'Статичний метод успадкований від Animal, тому Dog отримує версію батька. Якщо підклас має повертати своє значення, потрібен override або виклик через super:
class Bird extends Animal {
static getSpecies() { return super.getSpecies() + ' (Bird)'; }
}
console.log(Bird.getSpecies()); // 'Animal (Bird)'Де зустрічається в реальних проєктах
Math.max(),Math.min(),Array.isArray(),Promise.all()- стандартна бібліотека побудована на статикахpath.join()у Node.js - чиста утиліта без потреби в екземпляріexpress.json()- фабрика middleware у стилі статичного методуDate.now()- повертає timestamp без створення об'єкта Date- Утиліти Lodash на кшталт
_.debounce()слідують тому ж патерну: без стану, без екземпляра
Питання на співбесіді
Q: Чим статичні методи відрізняються від методів прототипу на рівні реалізації?
A: Методи прототипу зберігаються на Class.prototype й доступні через ланцюжок прототипів під час виклику instance.method(). Статичні методи - це властивості безпосередньо на конструкторі. Для статиків обходу ланцюжка немає.
Q: Чи може статичний метод отримати доступ до властивостей екземпляра?
A: Напряму - ні. Всередині статика this - це клас, а не екземпляр. Якщо потрібні дані об'єкта, передай його аргументом: MyClass.process(someInstance).
Q: Що таке this всередині статичного методу?
A: Функція-конструктор класу. Тобто MyClass.someStatic() прив'язує this до MyClass, а не до жодного об'єкта, створеного через new.
Q: Коли краще використати звичайну функцію замість статичного методу?
A: Коли хелпер не пов'язаний з конкретним класом. Статичний метод виправданий, коли групування під іменем класу додає зрозумілості. Якщо ні - вільна функція простіша.
Q: (Senior) Поясни спадкування статичних методів і ключове слово super у статиках.
A: Підкласи успадковують статичні методи батька через ланцюжок прототипів на рівні конструкторів: Dog.__proto__ === Animal. Тому Dog.getSpecies() знаходить Animal.getSpecies. В override можна викликати super.getSpecies(), щоб явно дістатися батьківського статика.
Приклади
Фабричний метод: створення екземпляра з рядка
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
static fromString(str) {
const [name, age] = str.split(', ');
return new Person(name, parseInt(age, 10));
}
}
const alice = Person.fromString('Alice, 30');
console.log(alice.name); // 'Alice'
console.log(alice.age); // 30fromString створює Person, не розкриваючи сигнатуру конструктора. Логіка створення в одному місці, і пізніше можна додати Person.fromJSON(json) чи інші фабрики без зміни точок виклику.
Валідатор вхідних даних у маршруті Express
class RouteValidator {
static validateUserId(id) {
const parsed = Number(id);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error('Invalid user ID');
}
return parsed;
}
}
app.get('/users/:id', (req, res) => {
try {
const userId = RouteValidator.validateUserId(req.params.id);
res.json({ userId });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// GET /users/123 -> { userId: 123 }
// GET /users/abc -> 400 { error: 'Invalid user ID' }Жодного екземпляра RouteValidator не створюється. Логіка валідації згрупована під зрозумілим ім'ям і повторно використовується в усіх маршрутах, де вона потрібна. Це найпоширеніший реальний патерн для статичних методів у Node.js бекендах.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.