Урок 14 из 49 · Месяц 2. Git и введение в JavaScript
Функции, анонимные функции, замыкания и колбэки
Содержание урока
- Что такое функция
- Параметры и аргументы
- return — вернуть результат
- Область видимости
- Способы объявить функцию
- Function Declaration
- Function Expression и анонимные функции
- Стрелочные функции
- Колбэки
- Замыкания
- Зачем нужны замыкания
- DOM и события на функциях
- Именованные обработчики
- Фабрика обработчиков через замыкание
- Делегирование событий
- Задержка и debounce
- Типичные ошибки
- Практика
- Итоги
Функции — главный строительный блок JavaScript: они позволяют один раз описать действие и использовать его сколько угодно раз. В этом уроке разберём, как объявлять функции, что такое параметры и return, чем отличаются анонимные и стрелочные функции, как работают замыкания и колбэки. А в конце применим всё это к DOM и событиям, чтобы код интерактивных страниц стал короче и чище.
Что такое функция
Функция — это именованный кусок кода, который можно вызвать в любой момент. Аналогия — рецепт: вы записали его один раз, а готовить по нему можете каждый день, подставляя разные продукты.
function greet() {
console.log("Привет!");
}
greet(); // Привет!
greet(); // Привет!
function— ключевое слово «объявляю функцию»;greet— имя функции (обычно глагол:show,calculate,get);()— место для параметров;{ ... }— тело функции, код, который выполнится при вызове;greet()— вызов: скобки после имени запускают функцию.
Параметры и аргументы
Функции можно передавать данные:
function greet(name) {
console.log(`Привет, ${name}!`);
}
greet("Айбек"); // Привет, Айбек!
greet("Мээрим"); // Привет, Мээрим!
name — параметр (переменная внутри функции), а "Айбек" — аргумент (значение, переданное при вызове). Параметров может быть несколько, и им можно задать значения по умолчанию:
function createUser(name, role = "student") {
console.log(`${name} — ${role}`);
}
createUser("Данияр"); // Данияр — student
createUser("Асель", "mentor"); // Асель — mentor
return — вернуть результат
Чаще всего функция не печатает, а возвращает значение, чтобы его можно было использовать дальше:
function sum(a, b) {
return a + b;
}
const result = sum(2, 3);
console.log(result); // 5
console.log(sum(10, 20) * 2); // 60
return делает две вещи: отдаёт значение наружу и сразу завершает функцию — код после него не выполнится. Если return нет, функция возвращает undefined.
function checkAge(age) {
if (age < 18) {
return "Доступ запрещён";
}
return "Добро пожаловать";
}
console.log(checkAge(15)); // Доступ запрещён
Область видимости
Переменные, объявленные внутри функции, не видны снаружи:
function calc() {
const secret = 42;
return secret * 2;
}
console.log(calc()); // 84
// console.log(secret); // ReferenceError: secret is not defined
А вот функция видит переменные снаружи себя — это основа замыканий, о которых ниже.
Способы объявить функцию
Function Declaration
function multiply(a, b) {
return a * b;
}
Такую функцию можно вызвать даже до её объявления в коде — браузер заранее «поднимает» её (hoisting).
Function Expression и анонимные функции
Анонимная функция — функция без имени. Её можно положить в переменную:
const multiply = function (a, b) {
return a * b;
};
console.log(multiply(3, 4)); // 12
Такую функцию можно вызвать только после строки, где она создана. Анонимные функции чаще всего передают в другие функции — например, в addEventListener или setTimeout:
setTimeout(function () {
console.log("Прошло 2 секунды");
}, 2000);
Стрелочные функции
Короткий синтаксис из ES6:
const multiply = (a, b) => {
return a * b;
};
// если тело — одно выражение, можно без {} и return:
const square = (x) => x * x;
const sayHi = () => console.log("Hi");
console.log(square(5)); // 25
Правила записи:
- нет параметров — пустые скобки
() => ...; - один параметр — скобки можно опустить:
x => x * 2(но с ними привычнее); - тело без
{}автоматически возвращает результат; - чтобы вернуть объект без
return, оберните его в скобки:() => ({ id: 1 }).
| Declaration | Expression | Стрелочная | |
|---|---|---|---|
| Синтаксис | function f() {} |
const f = function () {} |
const f = () => {} |
| Вызов до объявления | Можно | Нельзя | Нельзя |
Свой this |
Есть | Есть | Нет, берёт снаружи |
Колбэки
Колбэк (callback, «обратный вызов») — функция, которую передают другой функции как аргумент, чтобы та вызвала её позже. Аналогия: вы оставляете в кафе номер телефона — «перезвоните, когда заказ будет готов». Номер телефона — это колбэк.
function processOrder(dish, onReady) {
console.log(`Готовим: ${dish}...`);
onReady(dish); // вызываем переданную функцию
}
processOrder("плов", (name) => {
console.log(`${name} готов, забирайте!`);
});
Разбор:
processOrderпринимает второй параметрonReady— это функция.- При вызове мы передаём туда стрелочную функцию — не вызывая её.
- Внутри
processOrderв нужный момент делаемonReady(dish).
Одну и ту же функцию можно использовать с разными колбэками — это делает код гибким:
function repeat(times, action) {
for (let i = 1; i <= times; i++) {
action(i);
}
}
repeat(3, (n) => console.log("Шаг", n));
repeat(2, (n) => console.log("★".repeat(n)));
Колбэки вы уже использовали: обработчик в addEventListener — колбэк, функция в setTimeout — тоже. Методы массивов map, filter, forEach тоже принимают колбэки — их разберём в уроке Методы перебора массива.
Замыкания
Замыкание — это способность функции помнить переменные того места, где она была создана, даже когда это место уже «закончило работу».
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
const another = createCounter();
console.log(another()); // 1 — у него свой count
Разбор по шагам:
createCounter()создаёт переменнуюcount = 0и возвращает внутреннюю функцию.createCounterзакончила работу, но внутренняя функция по-прежнему видитcount— она «замкнула» её в себе.- Каждый вызов
counter()увеличивает именно этотcount. - Новый вызов
createCounter()создаёт новую переменнуюcount— уanotherсвой независимый счётчик.
Аналогия: функция уходит из дома с рюкзаком, в котором лежат переменные этого дома. Куда бы она ни пошла, рюкзак при ней.
Зачем нужны замыкания
- Скрыть данные: к
countнельзя обратиться снаружи и случайно испортить — только через функцию. - Создавать настроенные функции:
function makeDiscount(percent) {
return (price) => price - (price * percent) / 100;
}
const sale10 = makeDiscount(10);
const sale50 = makeDiscount(50);
console.log(sale10(1000)); // 900
console.log(sale50(1000)); // 500
sale10 и sale50 — разные функции, каждая помнит свой percent.
DOM и события на функциях
В уроке Введение в DOM мы писали обработчики прямо внутри addEventListener. Теперь вынесем логику в функции — код станет понятнее и его можно переиспользовать.
Именованные обработчики
<button id="like-btn">♡ Нравится</button>
<span id="likes">0</span>
const likeBtn = document.querySelector("#like-btn");
const likesEl = document.querySelector("#likes");
let likes = 0;
let liked = false;
function render() {
likesEl.textContent = likes;
likeBtn.textContent = liked ? "♥ Нравится" : "♡ Нравится";
}
function handleLike() {
liked = !liked;
likes += liked ? 1 : -1;
render();
}
likeBtn.addEventListener("click", handleLike);
render();
renderотвечает только за отображение;handleLike— за логику;- у обработчика есть имя, поэтому его можно снять:
likeBtn.removeEventListener("click", handleLike). Анонимную функцию так снять нельзя — на неё нет ссылки.
Фабрика обработчиков через замыкание
Нужно сделать несколько кнопок, которые меняют размер шрифта:
<button class="size-btn" data-size="14">Мелкий</button>
<button class="size-btn" data-size="18">Средний</button>
<button class="size-btn" data-size="24">Крупный</button>
<p id="article">Пример текста статьи.</p>
const article = document.querySelector("#article");
function makeSizeHandler(size) {
return () => {
article.style.fontSize = `${size}px`;
};
}
const buttons = document.querySelectorAll(".size-btn");
for (const btn of buttons) {
btn.addEventListener("click", makeSizeHandler(btn.dataset.size));
}
makeSizeHandler(size) возвращает функцию-обработчик, которая через замыкание помнит свой size. Атрибут data-size в HTML читается в JavaScript как btn.dataset.size.
Делегирование событий
Если элементов много или они добавляются динамически, удобно повесить один обработчик на родителя. События «всплывают» от элемента вверх по дереву, поэтому родитель узнает о клике по любому ребёнку:
<ul id="menu">
<li data-page="home">Главная</li>
<li data-page="catalog">Каталог</li>
<li data-page="contacts">Контакты</li>
</ul>
<p id="current"></p>
const menu = document.querySelector("#menu");
const current = document.querySelector("#current");
function handleMenuClick(event) {
const item = event.target.closest("li");
if (!item) return; // клик мимо пункта
for (const li of menu.children) {
li.classList.remove("active");
}
item.classList.add("active");
current.textContent = `Открыта страница: ${item.dataset.page}`;
}
menu.addEventListener("click", handleMenuClick);
event.target.closest("li") находит ближайший li от места клика вверх по дереву — это работает, даже если внутри пункта будут иконки или span.
Задержка и debounce
Комбинация замыкания и колбэка — функция, которая запускает действие только после паузы во вводе (полезно для поиска). На странице есть поле <input id="search" />:
function debounce(callback, delay) {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => callback(...args), delay);
};
}
const search = document.querySelector("#search");
const handleSearch = debounce((event) => {
console.log("Ищем:", event.target.value);
}, 500);
search.addEventListener("input", handleSearch);
Замыкание хранит timerId между вызовами: каждое новое нажатие отменяет прошлый таймер, и поиск срабатывает только через 500 мс тишины. Запись ...args («собрать все аргументы») подробно разберём в уроке Деструктуризация, spread и rest.
Типичные ошибки
- Забыли
return— функция возвращаетundefined. - Код после
returnникогда не выполняется. - Вызов вместо передачи:
addEventListener("click", handle()). - Вызов function expression до объявления —
ReferenceError(дляconst). - Стрелка как метод объекта с
this—thisокажется не тем объектом. - Стрелка с
{}безreturn:(x) => { x * 2 }вернётundefined.
Практика
- Площадь. Напишите функцию
getArea(width, height), возвращающую площадь прямоугольника, и стрелочнуюgetPerimeter. Ожидаемый результат:getArea(3, 4)→12,getPerimeter(3, 4)→14. - Калькулятор с колбэком. Функция
calculate(a, b, operation)вызывает колбэкoperation. Передайте разные стрелочные функции для+,-,*. Ожидаемый результат:calculate(6, 3, (x, y) => x - y)→3. - Счётчик-замыкание. Сделайте
createCounter(start, step), возвращающую объект с методамиinc,dec,get. Ожидаемый результат: два счётчика работают независимо. - Цветные кнопки. Создайте 4 кнопки с
data-color. Через фабрику обработчиков сделайте так, чтобы клик менял фон страницы на этот цвет. Ожидаемый результат: фон меняется, обработчики созданы в цикле. - Список с делегированием. Список задач, где удаление по кнопке «✕» и отметка «выполнено» обрабатываются одним обработчиком на
ul. Ожидаемый результат: работает и для задач, добавленных после загрузки страницы.
Итоги
- Функция — переиспользуемый блок кода; параметры принимают данные,
returnотдаёт результат. - Есть три формы: declaration, expression (анонимная функция в переменной) и стрелочная.
- Колбэк — функция, переданная в другую функцию, чтобы её вызвали позже.
- Замыкание — функция помнит переменные места, где её создали.
- Обработчики событий лучше выносить в именованные функции, а для многих элементов использовать делегирование.