Добавить в корзинуПозвонить
Найти в Дзене
ovnoCod

Язык JavaScript - Декораторы и переадресация вызова, call/apply

Представьте, что у вас есть функция. Она делает своё дело. Но вам нужно добавить логирование. Или кэширование. Или проверку прав доступа. Или замер времени выполнения. Вы можете переписать функцию, добавив туда новый код. Но что, если функций много? Что, если вы не можете менять оригинал? Что, если вам нужно применить одно и то же улучшение к разным функциям? Встречайте декораторы - мощный паттерн, который позволяет оборачивать функции, добавляя им новое поведение без изменения исходного кода. А в основе декораторов лежат два метода - call и apply, которые позволяют вызывать функцию с произвольным контекстом this. Это как пульт управления для функций: вы решаете, что будет this внутри функции и какие аргументы она получит. Сегодня мы разберём всё: от простого логирования до сложных декораторов, от управления контекстом до создания кэширующих обёрток. Обычно this внутри функции определяется тем, как функция вызвана. Но иногда нам нужно сказать функции: "Забудь о том, как тебя вызвали, р
Оглавление
Картинка взята с ya.ru
Картинка взята с ya.ru

Невидимые посредники: Декораторы, переадресация вызова и магия call/apply

Представьте, что у вас есть функция. Она делает своё дело. Но вам нужно добавить логирование. Или кэширование. Или проверку прав доступа. Или замер времени выполнения. Вы можете переписать функцию, добавив туда новый код. Но что, если функций много? Что, если вы не можете менять оригинал? Что, если вам нужно применить одно и то же улучшение к разным функциям?

Встречайте декораторы - мощный паттерн, который позволяет оборачивать функции, добавляя им новое поведение без изменения исходного кода.

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

Сегодня мы разберём всё: от простого логирования до сложных декораторов, от управления контекстом до создания кэширующих обёрток.

Часть 1. call и apply: Ручное управление контекстом

Обычно this внутри функции определяется тем, как функция вызвана. Но иногда нам нужно сказать функции: "Забудь о том, как тебя вызвали, работай так, как будто тебя вызвали вот с этим this".

1.1 call - вызов с указанием this и аргументов по порядку

javascript

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

const user = { name: "Анна" };
const admin = { name: "Администратор" };

// Обычный вызов (this = undefined или global)
greet("Привет", "!");
// "Привет, undefined!"

// Вызов через call
greet.call(user, "Привет", "!");
// "Привет, Анна!"
greet.call(admin, "Здравствуйте", "...");
// "Здравствуйте, Администратор..."

1.2 apply - как call, но аргументы массивом

javascript

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

const user = { name: "Анна" };

// call: аргументы через запятую
greet.call(user, "Привет", "!");

// apply: аргументы массивом
greet.apply(user, ["Привет", "!"]);

// Разница особенно заметна, когда аргументы уже в массиве
const args = ["Привет", "!"];
greet.apply(user, args);

1.3 Когда использовать call и apply?

javascript

// 1. Заимствование методов у других объектов
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

// У arrayLike нет метода join, но мы можем заимствовать у массива
const str = Array.prototype.join.call(arrayLike, ", ");
console.log(str);
// "a, b, c"

// 2. Преобразование arguments в массив (старый способ)
function sum() {
const args = Array.prototype.slice.call(arguments);
return args.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4));
// 10

// 3. Нахождение максимума в массиве (до spread)
const numbers = [5, 1, 8, 3, 10];
const max = Math.max.apply(null, numbers);
console.log(max);
// 10

// Современный способ - spread
const maxModern = Math.max(...numbers);

Часть 2. Декораторы: Оборачиваем функции для расширения

Декоратор - это функция, которая принимает другую функцию и возвращает новую, с добавленным поведением.

javascript

// Простейший декоратор - логирование
function logger(originalFn) {
return function(...args) {
console.log(`Вызов функции ${originalFn.name} с аргументами:`, args);
const result = originalFn.apply(this, args);
console.log(`Результат:`, result);
return result;
};
}

function add(a, b) {
return a + b;
}

const loggedAdd = logger(add);
console.log(loggedAdd(2, 3));
// Вызов функции add с аргументами: [2, 3]
// Результат: 5
// 5

Часть 3. Декоратор задержки (delay)

javascript

function delay(fn, ms) {
return function(...args) {
setTimeout(() => {
fn.apply(this, args);
}, ms);
};
}

function sayHi(name) {
console.log(`Привет, ${name}!`);
}

const delayedHi = delay(sayHi, 2000);
delayedHi("Анна");
// "Привет, Анна!" - через 2 секунды

Часть 4. Декоратор throttle (троттлинг)

javascript

function throttle(fn, limit) {
let inThrottle = false;
let lastArgs = null;
let lastThis = null;

function execute() {
if (lastArgs) {
fn.apply(lastThis, lastArgs);
lastArgs = null;
lastThis = null;
setTimeout(execute, limit);
} else {
inThrottle = false;
}
}

return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(execute, limit);
} else {
lastArgs = args;
lastThis = this;
}
};
}

const throttledLog = throttle((msg) => {
console.log(msg, Date.now());
}, 1000);

throttledLog("Раз");
// выполнится
throttledLog("Два");
// не выполнится (ждёт)
setTimeout(() => throttledLog("Три"), 1100);
// выполнится через 1.1с

Часть 5. Декоратор debounce (дебонс)

javascript

function debounce(fn, delay) {
let timeoutId = null;

return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

const debouncedSearch = debounce((query) => {
console.log(`Поиск: ${query}`);
}, 300);

debouncedSearch("a");
debouncedSearch("ap");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple");
// только этот выполнится через 300мс

Часть 6. Декоратор once (одноразовое выполнение)

javascript

function once(fn) {
let executed = false;
let result;

return function(...args) {
if (!executed) {
executed = true;
result = fn.apply(this, args);
}
return result;
};
}

const initialize = once(() => {
console.log("Инициализация...");
return { status: "ready" };
});

console.log(initialize());
// "Инициализация..." → { status: "ready" }
console.log(initialize());
// { status: "ready" } (без повторной инициализации)

Часть 7. Декоратор мемоизации (кэширование результатов)

javascript

function memoize(fn) {
const cache = new Map();

return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Из кэша");
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

const slowFibonacci = function(n) {
if (n <= 1) return n;
return slowFibonacci(n - 1) + slowFibonacci(n - 2);
};

const fastFibonacci = memoize(slowFibonacci);
console.log(fastFibonacci(40));
// быстро!

Часть 8. Композиция декораторов

Декораторы можно комбинировать, накладывая один на другой.

javascript

// Сначала логируем, потом мемоизируем
const loggedAndMemoized = memoize(logger(slowFibonacci));

// Или наоборот
const memoizedAndLogged = logger(memoize(slowFibonacci));

// Порядок важен!

javascript

function compose(...decorators) {
return function(fn) {
return decorators.reduceRight((acc, decorator) => decorator(acc), fn);
};
}

const enhanced = compose(
logger,
memoize,
throttle(1000)
)(slowFunction);

Часть 9. Переадресация вызова (forwarding)

Иногда декоратору нужно передать вызов дальше, сохраняя this и все аргументы.

javascript

function forwardingDecorator(fn) {
return function(...args) {
// Делаем что-то до
console.log("До вызова");

// Переадресация вызова
const result = fn.apply(this, args);

// Делаем что-то после
console.log("После вызова");

return result;
};
}

const obj = {
value: 42,
getValue(x) {
return this.value + x;
}
};

obj.getValue = forwardingDecorator(obj.getValue);
console.log(obj.getValue(10));
// 52 (с логированием)

Часть 10. Декоратор проверки типов

javascript

function typeCheck(types) {
return function(fn) {
return function(...args) {
for (let i = 0; i < types.length; i++) {
const argType = typeof args[i];
const expectedType = types[i];
if (argType !== expectedType) {
throw new TypeError(
`Аргумент ${i} должен быть ${expectedType}, получен ${argType}`
);
}
}
return fn.apply(this, args);
};
};
}

const add = typeCheck(["number", "number"])((a, b) => a + b);
console.log(add(2, 3));
// 5
// console.log(add(2, "3")); // TypeError!

Часть 11. Декоратор измерения времени

javascript

function timing(fn) {
return function(...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${fn.name} выполнилась за ${end - start}мс`);
return result;
};
}

const heavyCalculation = timing(function(n) {
let sum = 0;
for (let i = 0; i < n; i++) sum += i;
return sum;
});

heavyCalculation(10000000);
// "heavyCalculation выполнилась за 12.5мс"

Часть 12. Декоратор ретрая (повторных попыток)

javascript

function retry(maxAttempts, delay = 0) {
return function(fn) {
return async function(...args) {
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn.apply(this, args);
} catch (error) {
lastError = error;
console.log(`Попытка ${attempt} из ${maxAttempts} не удалась`);
if (attempt < maxAttempts && delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

throw lastError;
};
};
}

const unstableFetch = retry(3, 1000)(async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Ошибка");
return response.json();
});

Часть 13. Привязка контекста через bind

bind - это метод, который создаёт новую функцию с навсегда привязанным this.

javascript

const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};

const greet = user.greet;
greet();
// "Привет, undefined" (потеряли this)

const boundGreet = user.greet.bind(user);
boundGreet();
// "Привет, Анна"

// bind также может привязывать аргументы (частичное применение)
function multiply(a, b) {
return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5));
// 10
console.log(double(10));
// 20

Часть 14. Реальный кейс: Система плагинов

javascript

class PluginSystem {
constructor() {
this.plugins = [];
}

use(plugin) {
this.plugins.push(plugin);
return this;
}

createDecorator() {
return (fn) => {
return this.plugins.reduceRight((acc, plugin) => {
return plugin(acc);
}, fn);
};
}
}

const system = new PluginSystem();
system
.use(logger)
.use(timing)
.use(memoize);

const enhance = system.createDecorator();
const enhancedFn = enhance(expensiveFunction);

Часть 15. Подводные камни

Камень #1: Потеря this в декораторе

javascript

function badDecorator(fn) {
return function(...args) {
// ❌ Потеряли this!
return fn(...args);
};
}

function goodDecorator(fn) {
return function(...args) {
// ✅ Сохраняем this
return fn.apply(this, args);
};
}

Камень #2: Декораторы и асинхронные функции

javascript

function asyncLogger(fn) {
return async function(...args) {
console.log("Начало асинхронной операции");
const result = await fn.apply(this, args);
console.log("Конец асинхронной операции");
return result;
};
}

const fetchData = asyncLogger(async (url) => {
const response = await fetch(url);
return response.json();
});

Камень #3: Декоратор, меняющий сигнатуру

javascript

function addFlag(fn) {
return function(...args) {
// ❌ Добавили флаг, изменили сигнатуру
return fn.apply(this, [...args, true]);
};
}

// Лучше: возвращайте объект или используйте контекст
function withMetadata(fn) {
return function(...args) {
const result = fn.apply(this, args);
return { result, timestamp: Date.now() };
};
}

Итог: Манифест декораторов и call/apply

  1. call - вызывает функцию с указанным this и аргументами через запятую.
  2. apply - как call, но аргументы массивом.
  3. bind - создаёт новую функцию с привязанным this и аргументами.
  4. Декоратор - функция, оборачивающая другую функцию для добавления поведения.
  5. Логирование, задержка, throttle, debounce, once, мемоизация - классические декораторы.
  6. Всегда сохраняйте this - используйте fn.apply(this, args) в декораторах.
  7. Композиция - декораторы можно накладывать друг на друга.
  8. Для асинхронных функций - декоратор должен возвращать async функцию.

Финальный тест (что выведет?):

javascript

function foo() {
console.log(this.name);
}

const obj1 = { name: "Объект 1" };
const obj2 = { name: "Объект 2" };

foo.call(obj1);
foo.apply(obj2);
const bound = foo.bind(obj1);
bound.call(obj2);

function sum(a, b) {
return a + b;
}
const add5 = sum.bind(null, 5);
console.log(add5(3));

Ответы: "Объект 1", "Объект 2", "Объект 1" (bind сильнее), 8.

Декораторы и управление контекстом - это темы, которые разделяют новичков и профессионалов. Понимание call, apply, bind открывает дверь к функциональному программированию и метапрограммированию. А декораторы позволяют писать чистый, модульный и расширяемый код. Освойте эти инструменты, и ваши функции станут не просто исполнителями, а гибкими, настраиваемыми конструкциями, готовыми к любым улучшениям.