call и apply в JavaScript: Топ ошибок и как их избежать
Вы когда-нибудь смотрели на чужой код на JavaScript и видели странные конструкции вроде func.call(this, arg1, arg2) или func.apply(this, [arg1, arg2])? И задавались вопросом: "А в чём, собственно, разница?" Если да — вы не одиноки. Вопрос "What is the difference between call and apply?" — один из самых популярных на Stack Overflow с сотнями тысяч просмотров. И это не случайно.
Путаница между call и apply — классическая ловушка для новичков (и не только). В этой статье мы не просто расскажем теорию. Мы разберём топ-5 реальных ошибок, покажем, как их исправить, и дадим чёткий алгоритм, который навсегда избавит вас от путаницы. Поехали!
Краткий ликбез: что такое call и apply?
Прежде чем нырять в ошибки, давайте освежим базу. Методы call и apply — это встроенные методы функций в JavaScript. Их главная задача — явно указать контекст вызова (this) для функции. Другими словами, они позволяют сказать: "Эй, функция, представь, что ты работаешь вот с этим объектом".
Синтаксис:
func.call(context, arg1, arg2, ...);
func.apply(context, [arg1, arg2, ...]);
Ключевое отличие:
callпринимает аргументы через запятую (список).applyпринимает аргументы в виде массива (или массивоподобного объекта).
Всё. Это единственное различие. Но именно из-за этого "пустяка" возникает большинство ошибок. Давайте разберём их.
Топ-5 ошибок при работе с call и apply
Ошибка №1: Путаница со списком аргументов (самая частая!)
Это "король" всех ошибок. Вы написали функцию, которая ожидает аргументы по одному, а передали массив через call. Или наоборот.
Пример-катастрофа:
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const user = { name: 'Анна' };
// Ошибка: передаём массив, но используем call
greet.call(user, ['Привет', '!']);
// Вывод: "Привет,! Анна,undefined" — полная ерунда!
Почему так? call ожидает аргументы по одному. Он получил один аргумент — целый массив — и поместил его в переменную greeting. punctuation осталась undefined.
Как исправить? Используйте apply, если данные уже в массиве:
greet.apply(user, ['Привет', '!']);
// Вывод: "Привет, Анна!" — правильно!
Или разверните массив через spread-оператор (современный подход):
greet.call(user, ...['Привет', '!']);
// Тоже работает!
Ошибка №2: Забываем, что apply работает с массивоподобными объектами
Многие думают, что apply принимает только настоящий массив. На самом деле он принимает любой массивоподобный объект (array-like object): у него должно быть свойство length и числовые ключи.
Пример:
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// Передаём не массив, а объект с length
const args = { 0: 5, 1: 10, 2: 15, length: 3 };
console.log(sum.apply(null, args)); // Вывод: 30 — работает!
Но будьте осторожны: если у объекта нет length, apply просто проигнорирует его. Это частая ошибка при работе с arguments (устаревший объект) или с NodeList.
Ошибка №3: Использование call/apply там, где достаточно простого вызова
Иногда новички злоупотребляют call и apply, хотя в этом нет никакой необходимости. Это усложняет код и делает его менее читаемым.
Плохой пример:
function sayHello() {
console.log('Привет, ' + this.name);
}
const person = { name: 'Олег' };
// Зачем? Можно проще!
sayHello.call(person); // Работает, но...
Лучше:
// Просто передайте контекст через обычный аргумент
function sayHello(context) {
console.log('Привет, ' + context.name);
}
sayHello(person); // Чисто и понятно
Используйте call и apply только когда действительно нужно переопределить this. Иначе — обычные аргументы или методы объекта.
Ошибка №4: Неправильный контекст в setTimeout и обработчиках событий
Классика жанра. Вы передаёте метод объекта в setTimeout, и this "теряется".
Пример:
const timer = {
seconds: 0,
start: function() {
// Ошибка: this будет указывать на window (или undefined в strict mode)
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
};
timer.start(); // Вывод: NaN, NaN, NaN...
Решение через call/apply:
const timer = {
seconds: 0,
start: function() {
const self = this; // сохраняем контекст
setInterval(function() {
self.seconds++;
console.log(self.seconds);
}, 1000);
}
};
// Или используем bind (рекомендуется)
start: function() {
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
}
call и apply тут не подходят напрямую, потому что они немедленно вызывают функцию. Для отложенного вызова используйте bind (он возвращает новую функцию с привязанным контекстом).
Ошибка №5: Забываем про null/undefined в качестве контекста
Если вы передаёте null или undefined как первый аргумент в call или apply, то в нестрогом режиме this будет указывать на глобальный объект (window в браузере, global в Node.js). В строгом режиме — будет