call и apply

call и apply — методы Function.prototype, позволяющие вызвать функцию с явно указанным контекстом (this) и аргументами. Отличаются способом передачи аргументов.

Зачем нужно

call и apply дают полный контроль над значением this при вызове функции. Это нужно для заимствования методов, реализации полиморфизма, работы с функциями-обёртками и делегирования вызовов.

Где используется

Заимствование методов (method borrowing), вызов функции с динамическим контекстом, передача аргументов из массива, реализация наследования (до ES6), декораторы, polyfills.

Предпосылки

this, Function Declaration

Синтаксис

// call — аргументы через запятую
fn.call(thisArg, arg1, arg2, ...);

// apply — аргументы массивом
fn.apply(thisArg, [arg1, arg2, ...]);

call — вызов с контекстом

function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const user1 = { name: 'Иван' };
const user2 = { name: 'Мария' };

console.log(greet.call(user1, 'Привет', '!'));
// "Привет, Иван!"

console.log(greet.call(user2, 'Здравствуйте', '.'));
// "Здравствуйте, Мария."

apply — вызов с массивом аргументов

function introduce(greeting, role) {
  return `${greeting}, я ${this.name}, ${role}`;
}

const dev = { name: 'Алиса' };
const args = ['Привет', 'разработчик'];

console.log(introduce.apply(dev, args));
// "Привет, я Алиса, разработчик"

Разница между call и apply

// call — аргументы списком
Math.max.call(null, 1, 2, 3); // 3

// apply — аргументы массивом
Math.max.apply(null, [1, 2, 3]); // 3

// Мнемоника:
// Call  — Comma (запятые)
// Apply — Array (массив)

// В современном JS spread заменяет apply:
Math.max(...[1, 2, 3]); // 3

Заимствование методов (Method Borrowing)

Array-методы для array-like объектов

// arguments — не массив, но похож на массив
function example() {
  // Заимствуем Array.prototype.slice
  const args = Array.prototype.slice.call(arguments);
  console.log(args); // настоящий массив

  // Заимствуем forEach
  Array.prototype.forEach.call(arguments, (arg) => {
    console.log(arg);
  });

  // Современная альтернатива:
  const argsModern = Array.from(arguments);
  const argsSpread = [...arguments];
}

example(1, 2, 3);

Заимствование toString

// Определение реального типа объекта
function getType(value) {
  return Object.prototype.toString().call(value);
}

console.log(getType());        // "[object Array]"
console.log(getType({}));        // "[object Object]"
console.log(getType(null));      // "[object Null]"
console.log(getType(undefined)); // "[object Undefined]"
console.log(getType(42));        // "[object Number]"
console.log(getType(/regex/));   // "[object RegExp]"

Заимствование метода у другого объекта

const dog = {
  name: 'Бобик',
  speak {
    return `${this.name} лает: Гав!`;
  }
};

const cat = {
  name: 'Мурка'
};

// Кот использует метод собаки
console.log(dog.speak.call(cat));
// "Мурка лает: Гав!" — работает, но абсурдно :)

Практические примеры

Наследование конструкторов (до ES6)

function Animal(name, sound) {
  this.name = name;
  this.sound = sound;
}

function Dog(name) {
  // Вызываем конструктор родителя с нашим this
  Animal.call(this, name, 'Гав');
  this.tricks = ;
}

const dog = new Dog('Рекс');
console.log(dog.name);  // 'Рекс'
console.log(dog.sound); // 'Гав'

Применение apply для spread аргументов

// До spread-оператора
const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers); // 7
const min = Math.min.apply(null, numbers); // 2

// Современный эквивалент
const maxModern = Math.max(...numbers);

Вызов с null/undefined

// Когда контекст не важен — передаём null
function sum(a, b) {
  return a + b;
}

sum.call(null, 1, 2);    // 3
sum.apply(null, [1, 2]);  // 3

// Осторожно: в non-strict mode null/undefined заменяется на globalThis
// В strict mode this остаётся null/undefined

Динамический вызов метода

const operations = {
  add(a, b) { return a + b; },
  subtract(a, b) { return a - b; },
  multiply(a, b) { return a * b; },
};

function execute(operation, ...args) {
  if (operations[operation]) {
    return operations[operation].apply(null, args);
  }
  throw new Error(`Неизвестная операция: ${operation}`);
}

console.log(execute('add', 5, 3));      // 8
console.log(execute('multiply', 4, 7)); // 28

Декоратор с сохранением контекста

function withLogging(fn) {
  return function(...args) {
    console.log(`Вызов ${fn.name} с`, args);
    // Сохраняем this через apply
    const result = fn.apply(this, args);
    console.log(`Результат:`, result);
    return result;
  };
}

const user = {
  name: 'Иван',
  greet: withLogging(function greet(greeting) {
    return `${greeting}, ${this.name}`;
  })
};

user.greet('Привет');
// Вызов greet с ['Привет']
// Результат: Привет, Иван

Reflect.apply (ES6)

// Современная альтернатива Function.prototype.apply
function sum(a, b) {
  return a + b;
}

const result = Reflect.apply(sum, null, [1, 2]);
console.log(result); // 3

// Полезно когда apply может быть переопределён

Частые ошибки

1. Путаница call и apply

function test(a, b, c) {
  console.log(a, b, c);
}

// Неправильно — передали массив как ОДИН аргумент в call
test.call(null, [1, 2, 3]); // [1, 2, 3] undefined undefined

// Правильно
test.call(null, 1, 2, 3);     // 1 2 3
test.apply(null, [1, 2, 3]);  // 1 2 3

2. Забытый контекст в декораторе

// Плохо — потеря this
function bad(fn) {
  return function(...args) {
    return fn(...args); // this не передаётся!
  };
}

// Хорошо — apply сохраняет this
function good(fn) {
  return function(...args) {
    return fn.apply(this, args);
  };
}

3. Вызов с примитивом как this

function test() {
  console.log(typeof this, this);
}

// В non-strict mode: примитив оборачивается в объект
test.call(42);     // "object" Number {42}
test.call('hello'); // "object" String {'hello'}

// В strict mode: остаётся примитивом
// "number" 42

Практика

  1. Напиши функцию greet и вызови её с разными контекстами через call
  2. Используй apply для передачи массива аргументов в Math.max
  3. Заимствуй метод Array.prototype.join для arguments-объекта
  4. Реализуй функцию invoke(obj, methodName, ...args) через call
  5. Напиши декоратор, который сохраняет контекст через apply

Связанные темы

Ресурсы


🎓 Источник: Функции, стрелочные функции, контексты

  • 📅 2018-09-27 · YouTube · ID: pn5myCmpV2U
  • Тезисы:
    • f1.call(null, 2, 3) — аргументы через запятую, первый — this
    • f1.apply(null, [2, 3]) — массив аргументов, первый — this
    • Spread-оператор f1(...arr) — эквивалент f1.apply(null, arr)
    • Spread на вызове разбивает массив на аргументы — современная альтернатива apply
  • Цитата: «Я могу сюда передать какой-то объект, и этот объект будет виден в функции F1 через идентификатор this.»

⚡ Источник: Как работает this · AsForJS

  • 📅 2023-05-07 · YouTube · ID: 4tg4qokVS9o
  • Тезисы:
    • call/apply/bind — высший приоритет для задания this
    • this через call/apply может быть любым значением, даже примитивом: fn.call(1)this === 1 (strict)
    • В non-strict примитив проходит ToObject и оборачивается
    • На стрелочных функциях call/apply для this НЕ работают