Изучаем функции
Введение
В языках C/C++ функция — это, по сути, указатель на блок кода в памяти. В JavaScript функция — это значение, объект первого класса (first-class citizen). Она может всё то же, что и переменная: храниться в массиве, передаваться как аргумент и, самое интересное, возвращаться из другой функции.
Если в C++ или Pascal функции — это «глаголы» (сделай то, сделай это), то в JavaScript функция может быть инструментом, который создаёт другие узкоспециализированные инструменты. Это делает код гибким, как конструктор LEGO. Однако для понимания таких возможностей нужно разобраться с контекстом выполнения, областью видимости и замыканиями.
1. Способы объявления функций
- Function Declaration – привычный синтаксис:
function name() {}. - Function Expression – функция как значение переменной:
const name = function() {};. - Arrow Functions – лаконичный синтаксис, отсутствие своего
this.
2. Параметры и аргументы
- Значения параметров по умолчанию.
- Rest-оператор (
...args) – собирает все переданные аргументы в массив.
3. Область видимости и замыкания (Closures)
Глобальная и локальная области видимости
Переменные, объявленные внутри функции, недоступны снаружи.
Замыкание
Замыкание — это способность функции «запоминать» своё лексическое окружение (переменные родительской функции) даже после того, как родительская функция завершила работу.
Пример 1: Счётчик (инкапсуляция)
function createCounter() {
let count = 0; // «скрытая» переменная
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
const otherCounter = createCounter();
console.log(otherCounter()); // 1
// Доступа к переменной count извне нет
Пример 2: Генератор скидок (бизнес-кейс)
function createDiscount(percent) {
// Внутренняя функция «запоминает» значение percent
return function(price) {
const discountAmount = price * (percent / 100);
return price - discountAmount;
};
}
const applyBlackFriday = createDiscount(50);
const applySeasonal = createDiscount(20);
const applySilverStatus = createDiscount(10);
const laptopPrice = 100000;
console.log(applyBlackFriday(laptopPrice)); // 50000
console.log(applySeasonal(laptopPrice)); // 80000
console.log(applySilverStatus(laptopPrice)); // 90000
Вопрос: Где физически хранится percent после завершения createDiscount?
Ответ: В замыкании. Память не освобождается, пока существует ссылка на возвращённую функцию.
Вопрос: Можно ли изменить процент скидки у уже созданной функции?
Ответ: Нет, если не предусмотрен специальный метод. Это и есть инкапсуляция.
4. Функции высшего порядка
Функция высшего порядка — это функция, которая принимает другую функцию как аргумент и/или возвращает функцию.
4.1 Функция как аргумент (callback)
function sayHello() {
console.log("Hello");
}
function run(fn) {
fn(); // вызываем переданную функцию
}
run(sayHello); // передаётся сама функция, не результат её вызова
Важно: sayHello — это функция, sayHello() — результат её вызова.
4.2 Массив функций
const actions = [
() => console.log("AA"),
() => console.log("BB"),
() => console.log("CC")
];
actions[1](); // BB
Зачем нужен массив функций?
Пример: множество фильтров для массива товаров.
let filters = [];
filters[0] = el => el.price > 1000;
filters[1] = el => el.rate > 3.5;
filters[2] = el => el.categories === 'шина';
products.filter(prod => filters.every(fn => fn(prod)));
4.3 Возврат функции из функции
Возврат функции позволяет создавать гибкий и безопасный код, который генерирует нужные инструменты «на лету». Рассмотрим несколько типичных ситуаций.
4.3.1 Валидаторы (защита от дурака)
function minLength(min) {
return function(text) {
return text.length >= min;
};
}
const checkPassword = minLength(8);
const checkUsername = minLength(3);
console.log(checkPassword("123")); // false
console.log(checkUsername("jd")); // false
4.3.2 Обработчики событий (UI)
function createDeleteHandler(itemId) {
return function(event) {
console.log(`Удаляем объект с ID: ${itemId}`);
// здесь запрос к API
};
}
// При создании кнопки:
// button.onclick = createDeleteHandler(item.id);
Почему нельзя просто deleteItem(id)?
Потому что браузерный обработчик передаёт в функцию только объект события. Замыкание позволяет «склеить» нужный id и событие.
4.3.3 Персонализация (шаблоны приветствий)
function createGreeter(phrase) {
return function(name) {
return `${phrase}, ${name}!`;
};
}
const sayHi = createGreeter("Привет");
const sayHello = createGreeter("Здравствуйте");
console.log(sayHi("Иван")); // "Привет, Иван!"
console.log(sayHello("Петр")); // "Здравствуйте, Петр!"
4.3.4 Настраиваемая сортировка
const users = [
{ name: 'Ivan', age: 25, group: 3 },
{ name: 'Anna', age: 30, group: 5 },
{ name: 'Boris', age: 20, group: 1 }
];
function sortBy(field) {
return function(a, b) {
if (a[field] > b[field]) return 1;
if (a[field] < b[field]) return -1;
return 0;
};
}
users.sort(sortBy('name')); // сортировка по имени
users.sort(sortBy('age')); // по возрасту
users.sort(sortBy('group')); // по группе
4.3.5 Форматтер валют
function createFormatter(symbol, position = 'after') {
return function(amount) {
const fixedAmount = amount.toFixed(2);
return position === 'before'
? `${symbol}${fixedAmount}`
: `${fixedAmount}${symbol}`;
};
}
const toUSD = createFormatter('$', 'before');
const toEUR = createFormatter('€');
console.log(toUSD(100)); // "$100.00"
console.log(toEUR(45.5)); // "45.50€"
4.3.6 Частичное применение (Partial Application)
function createRequester(baseUrl) {
return function(endpoint) {
return function(params) {
console.log(`Sending GET to ${baseUrl}${endpoint} with query ${params}`);
};
};
}
const myApi = createRequester('https://api.myapp.com');
const getUsers = myApi('/users'); // зафиксировали домен и эндпоинт
getUsers('?id=42'); // финальный вызов
5. Чистые функции и побочные эффекты
В веб-разработке важно минимизировать изменение глобальных состояний. Чистая функция:
- всегда возвращает одинаковый результат для одних и тех же аргументов;
- не вызывает побочных эффектов (не меняет внешние переменные, не пишет в консоль и т.д.).
6. Продвинутые паттерны (middle level)
6.1 Debounce (оптимизация производительности)
function debounce(callback, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => callback(...args), delay);
};
}
const search = (query) => console.log("Запрос в БД:", query);
const optimizedSearch = debounce(search, 500);
6.2 Memoize (кэширование результатов)
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) {
return cache[n];
}
let result = fn(n);
cache[n] = result;
return result;
};
}
const slowSquare = (x) => x * x;
const fastSquare = memoize(slowSquare);
7. Мастер-уровень (master level)
7.1 Настраиваемая сортировка со стратегиями
const Strategies = {
default: (a, b) => a - b,
text: (a, b) => a.localeCompare(b, 'ru', { sensitivity: 'base' }),
date: (a, b) => new Date(a) - new Date(b),
priority: (order) => (a, b) => {
const weights = order;
return weights[a] - weights[b];
}
};
function sortBy(field, strategy = Strategies.default) {
return function(objA, objB) {
return strategy(objA[field], objB[field]);
};
}
const users = [
{ name: 'Ivan', role: 'guest', regDate: '2023-05-01' },
{ name: 'Anna', role: 'admin', regDate: '2022-12-15' },
{ name: 'Борис', role: 'editor', regDate: '2024-01-10' }
];
// Сортировка по имени с текстовой стратегией
const sortedByName = users.toSorted(sortBy('name', Strategies.text));
console.log(sortedByName);
// Сортировка по роли с приоритетами
const roleWeight = { admin: 3, editor: 2, guest: 1 };
users.sort(sortBy('role', Strategies.priority(roleWeight)));
// Добавим направление сортировки
function withDirection(comparator, reverse = false) {
return function(a, b) {
return reverse ? comparator(a, b) * -1 : comparator(a, b);
};
}
users.sort(withDirection(sortBy('name'), true)); // обратный порядок
7.2 Композиция фильтров (Filter Factory)
const products = [
{ name: 'iPhone', price: 1000, category: 'electronics', stock: 5 },
{ name: 'Book', price: 20, category: 'books', stock: 100 },
{ name: 'Laptop', price: 1500, category: 'electronics', stock: 0 },
{ name: 'T-shirt', price: 25, category: 'clothes', stock: 10 }
];
const priceAbove = (min) => (item) => item.price >= min;
const isCategory = (cat) => (item) => item.category === cat;
const inStock = () => (item) => item.stock > 0;
function and(...filters) {
return function(item) {
return filters.every(filterFunc => filterFunc(item));
};
}
const complexFilter = and(
isCategory('electronics'),
priceAbove(500),
inStock()
);
const result = products.filter(complexFilter);
console.log(result); // [ { name: 'iPhone', ... } ]
Заключение
В C для подобного функционала пришлось бы создавать массивы указателей на функции и передавать их в другие функции — это громоздко. В JavaScript мы можем строить «конструкторы поведения»: функции, которые принимают стратегии или правила и возвращают готовые к использованию специализированные функции, используя замыкания для сохранения контекста. Такой подход делает код декларативным, гибким и легко расширяемым.
Ключевые идеи:
- Функции — это значения.
- Замыкания позволяют инкапсулировать данные и создавать функции с «памятью».
- Возврат функций — мощный инструмент конфигурации, оптимизации и построения архитектуры.