call и apply в JavaScript: топ ошибок и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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). В строгом режиме — будет

Блоги

Рекомендаций по книгам