Методи call, apply та bind у JavaScript
call, apply та bind — методи Function.prototype, які дозволяють явно задати this для будь-якого виклику функції, незалежно від того, де і як її визначено.
Теорія
TL;DR
- Уявляй це як передачу функції конкретного об'єкта як її "власника" для цього виклику
callіapplyзапускають функцію одразу;bindповертає нову функцію з зафіксованимthis- Єдина різниця між
callіapply— формат аргументів: окремі значення чи масив - Правило вибору: треба виконати зараз —
callабоapply. Потрібна функція з постійним контекстом —bind bindтакож дозволяє зафіксувати аргументи наперед (часткове застосування, partial application)
Короткий приклад
const user = { name: 'Alice' };
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
greet.call(user, 'Hi', '!'); // "Hi, Alice!"
greet.apply(user, ['Hello', '.']); // "Hello, Alice."
const boundGreet = greet.bind(user, 'Hey');
boundGreet('!'); // "Hey, Alice!"call і apply виконуються одразу з заданим this. bind повертає нову функцію, яку ти викликаєш пізніше, і this у ній вже зафіксований.
Головна різниця
call і apply викликають функцію негайно з фіксованим this. Відрізняються лише форматом аргументів: call приймає їх окремо, apply приймає масив. bind не викликає функцію одразу. Він створює новий функціональний об'єкт із постійно закріпленим this і необов'язковими зафіксованими аргументами, який ти викликаєш тоді, коли треба.
Коли використовувати
- Позичаєш метод одного об'єкта для іншого:
callабоapply - Треба передати динамічний масив як аргументи функції:
apply - Передаєш метод як колбек і хочеш зберегти
this:bind - Фіксуєш частину аргументів для пізніших викликів:
bind - Виправляєш втрату
thisуsetTimeout, обробниках подій або React-методах:bind
Таблиця порівняння
call | apply | bind | |
|---|---|---|---|
| Виконується одразу | Так | Так | Ні |
| Повертає нову функцію | Ні | Ні | Так |
| Формат аргументів | Окремі значення | Масив | Окремі значення (зафіксовані) |
| Часткове застосування | Ні | Ні | Так |
Працює з new | Так | Так | Ні (TypeError) |
| Типовий сценарій | Позичити метод | Передати масив як аргументи | Колбек з фіксованим контекстом |
Як це працює всередині
Коли ти викликаєш fn.call(obj, arg), рушій встановлює [[ThisValue]] у поточному кадрі виклику на obj до запуску fn. apply робить те саме, але спочатку розпаковує масив у позиційні аргументи. bind створює новий екзотичний об'єкт BoundFunction із зафіксованим слотом [[BoundThis]] і масивом [[BoundArgs]]. При наступному виклику зв'язаної функції рушій підставляє зафіксовані аргументи, відновлює this і делегує виконання оригінальній функції. Зв'язані функції не мають поведінки [[Construct]], тому передача такої функції в new кидає TypeError.
Типові помилки
1. Очікування що bind виконає функцію одразу
const obj = { x: 10 };
function log() { console.log(this.x); }
log.bind(obj); // нічого не відбувається - повертає функцію, не викликає їїbind повертає нову функцію. Виклич її окремо: const bound = log.bind(obj); bound();
2. Передача не-масиву в apply
function add(a, b) { return a + b; }
add.apply(null, 1, 2); // TypeError: CreateListFromArrayLike called on non-objectapply очікує масив другим аргументом. Виправлення: add.apply(null, [1, 2]) або add.call(null, 1, 2).
3. Ланцюжок bind з очікуванням нового контексту
function show() { console.log(this.name); }
const a = show.bind({ name: 'Alice' });
const b = a.bind({ name: 'Bob' });
b(); // "Alice" - перший bind перемагає завждиПісля того як this зафіксований першим bind, жоден наступний це не змінить. Слот [[BoundThis]] встановлюється один раз і залишається.
4. Використання зв'язаної функції з new
function Counter(start) { this.count = start; }
const BoundCounter = Counter.bind(null, 10);
new BoundCounter(); // TypeErrorЗв'язані функції не підтримують [[Construct]]. Якщо потрібне часткове застосування з конструктором, використовуй фабричну функцію.
5. bind(null) у strict mode
'use strict';
function fn() { console.log(this); }
fn.bind(null)(); // null (не глобальний об'єкт)У звичайному режимі null перетворюється на глобальний об'єкт. У strict mode залишається null. Знай, в якому режимі виконується твій код.
Цей патерн bind-у-конструкторі в React-компонентах я бачив у десятках кодових баз. Щойно хтось забуває його і отримує Cannot read property of undefined під час події, одразу стає зрозуміло навіщо bind взагалі існує.
Де зустрічається в реальних проектах
- React класові компоненти:
this.handleClick = this.handleClick.bind(this)у конструкторі, щоб зберегтиthisпри передачі методу як пропс - Express:
app.use(handler.bind(dbContext))для передачі з'єднання з базою даних у кожен обробник маршруту - DOM-події:
element.addEventListener('click', handler.bind(component)), щобthisзалишався компонентом, а не DOM-елементом - Часткове застосування:
const double = multiply.bind(null, 2)для створення спеціалізованих функцій із загальних Math.max.apply(null, числовийМасив)- класичний паттерн до появи spread, ще зустрічається у старих кодових базах
Питання на співбесіді
Q: Що станеться якщо передати примітив як thisArg у call?
A: У звичайному режимі примітив автоматично загортається в об'єкт-обгортку (наприклад, 5 стає Number {5}). У strict mode залишається примітивом. Уникай цього і передавай об'єкти.
Q: Чи можна перевизначити this через ланцюжок bind і call?
A: Ні. fn.bind(obj).call(otherObj) все одно використає obj як this. Перший bind перемагає. Зафіксований this не можна перевизначити ні через call, ні через ще один bind.
Q: Як написати поліфіл для bind?
A: Зафіксуй оригінальну функцію і boundThis у замиканні (closure), поверни нову функцію що викликає оригінал через .apply(boundThis, boundArgs.concat(newArgs)). Повноцінний поліфіл також перевіряє instanceof, щоб кинути помилку при спробі використати new.
Q: Чому call, apply і bind не впливають на this у стрілкових функціях?
A: Стрілкові функції захоплюють this лексично в момент свого визначення. У них немає власного [[ThisValue]]. Будь-який thisArg ігнорується для this, але аргументи передаються нормально.
Q: Чи може apply замінити spread-оператор при передачі масивів?
A: Так, fn.apply(null, arr) і fn(...arr) функціонально еквівалентні. У сучасному коді spread зручніший, але apply ще зустрічається в старих кодових базах і в завданнях на написання поліфілів.
Приклади
Базовий: позичання методу між об'єктами
const dog = { sound: 'woof' };
const cat = { sound: 'meow' };
function makeNoise(times) {
for (let i = 0; i < times; i++) {
console.log(this.sound);
}
}
makeNoise.call(dog, 2); // "woof" "woof"
makeNoise.call(cat, 1); // "meow"Одна функція, два різні об'єкти як this. Без дублювання коду. Це і є основна причина існування call.
Середній: bind у React класовому компоненті
class Button extends React.Component {
constructor(props) {
super(props);
// прив'язуємо тут, щоб this.props працював при спрацюванні DOM-події
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Клікнуто:', this.props.label);
}
render() {
return <button onClick={this.handleClick}>{this.props.label}</button>;
}
}Без bind передача this.handleClick як пропс відриває метод від екземпляра. Коли спрацює DOM-подія, this буде undefined у strict mode. bind у конструкторі фіксує контекст один раз і він зберігається для всіх рендерів.
Просунутий: часткове застосування через bind
function fetchData(baseURL, endpoint, id) {
console.log(`GET ${baseURL}${endpoint}/${id}`);
}
// фіксуємо базовий URL для всіх API-запитів
const apiCall = fetchData.bind(null, 'https://api.example.com');
// фіксуємо endpoint для запитів користувачів
const getUser = apiCall.bind(null, '/users');
getUser(42); // "GET https://api.example.com/users/42"
getUser(99); // "GET https://api.example.com/users/99"Кожен bind фіксує ще більше аргументів. this тут null, бо fetchData його не використовує. У підсумку маємо ланцюжок спеціалізованих функцій із однієї загальної — без жодного зайвого коду-обгортки.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.