Пошук уроків, статей та іншого контенту
Дізнайтеся, як давати ім'я функції всередині Function Expression і чим це відрізняється від звичайної декларації. Розберіть користь іменованих функціональних виразів для рекурсії, налагодження та читабельного stack trace.
Функціональний вираз — це функція, яку створюють у складі іншого виразу та зазвичай присвоюють змінній:
const greet = function (name) {
return `Привіт, ${name}!`;
};У цьому прикладі функція не має власного імені. Звертатися до неї можна через змінну greet.
Іменований функціональний вираз містить ім’я після ключового слова function:
const greet = function greetUser(name) {
return `Привіт, ${name}!`;
};Тут:
greet — змінна, у яку збережено функцію;
greetUser — внутрішнє ім’я функції;
greetUser доступне всередині самої функції;
поза функцією звернутися до greetUser не можна.
const greet = function greetUser(name) {
console.log(greetUser === greet); // true
return `Привіт, ${name}!`;
};
console.log(greet("Олено")); // Привіт, Олено!
// console.log(greetUser); // ReferenceErrorІм’я функції та ім’я змінної — не одне й те саме.
Порівняймо декларацію функції та функціональний вираз.
function add(a, b) {
return a + b;
}У декларації ім’я add є частиною оголошення функції. Воно доступне в зовнішній області видимості.
Декларації функцій піднімаються на початок своєї області видимості, тому функцію можна викликати до рядка, де вона оголошена:
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}const add = function (a, b) {
return a + b;
};Функція створюється під час виконання цього виразу. До ініціалізації const викликати її не можна:
// console.log(add(2, 3)); // ReferenceError через TDZ
const add = function (a, b) {
return a + b;
};const add = function addition(a, b) {
return a + b;
};У цьому випадку:
add належить зовнішній області видимості;
addition є внутрішнім іменем функції;
вираз залишається саме функціональним виразом, а не декларацією.
Найчастіше іменовані функціональні вирази використовують для:
рекурсії;
зрозуміліших повідомлень про помилки;
читабельнішого stack trace;
стабільного посилання на функцію, незалежного від зовнішньої змінної.
Рекурсивна функція викликає сама себе. Для цього їй потрібне посилання на саму себе.
Рекурсію можна реалізувати через зовнішню змінну:
const factorial = function (n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
};Але такий підхід залежить від змінної factorial. Якщо змінити її значення, рекурсивний виклик працюватиме інакше або завершиться помилкою:
let factorial = function (n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
};
const originalFactorial = factorial;
factorial = null;
// originalFactorial(5) спричинить помилку,
// оскільки всередині функції factorial тепер має значення null.Іменований функціональний вираз має власне внутрішнє ім’я:
const factorial = function calculateFactorial(n) {
if (n <= 1) {
return 1;
}
return n * calculateFactorial(n - 1);
};
console.log(factorial(5)); // 120calculateFactorial доступне всередині тіла функції та посилається саме на цю функцію.
Це робить рекурсію більш надійною: зовнішню змінну можна переприсвоїти, але внутрішнє ім’я все одно посилатиметься на початкову функцію.
const factorial = function calculateFactorial(n) {
if (!Number.isInteger(n) || n < 0) {
throw new TypeError("n має бути невід’ємним цілим числом");
}
if (n <= 1) {
return 1;
}
return n * calculateFactorial(n - 1);
};
console.log(factorial(0)); // 1
console.log(factorial(5)); // 120
console.log(factorial(7)); // 5040Ім’я calculateFactorial також краще описує роль функції, ніж абстрактне factorial, якщо код читають у stack trace або під час налагодження.
Внутрішнє ім’я функції доступне лише в її тілі:
const operation = function multiply(a, b) {
console.log(multiply(2, 3)); // 6
return a * b;
};
operation(4, 5);
// console.log(multiply); // ReferenceErrorНавіть якщо зовнішня змінна має інше ім’я, функція використовує своє внутрішнє ім’я:
const calculate = function multiply(a, b) {
return a * b;
};
console.log(calculate(3, 4)); // 12У цьому прикладі:
змінна називається calculate;
функція називається multiply;
усередині функції можна звернутися до multiply;
у зовнішньому коді потрібно використовувати calculate.
Ім’я іменованого функціонального виразу є спеціальним внутрішнім прив’язуванням. Воно не призначене для переприсвоєння:
const fn = function namedFunction() {
// У строгому режимі таке переприсвоєння спричинить TypeError.
// namedFunction = null;
};Зазвичай внутрішнє ім’я використовують лише для виклику функції, рекурсії або діагностики.
Коли функція завершується помилкою, JavaScript створює stack trace — інформацію про послідовність викликів.
Іменовані функції допомагають читати цю інформацію:
const processOrder = function validateAndProcessOrder(order) {
if (!order || !order.id) {
throw new Error("Замовлення не має ідентифікатора");
}
return `Замовлення ${order.id} оброблено`;
};
processOrder({});У stack trace серед іншого можна побачити ім’я validateAndProcessOrder. Точний формат залежить від середовища виконання, наприклад браузера або Node.js, але власне ім’я функції зазвичай робить діагностику зрозумілішою.
Для порівняння, анонімний вираз може дати менш інформативний результат:
const processOrder = function (order) {
if (!order || !order.id) {
throw new Error("Замовлення не має ідентифікатора");
}
return `Замовлення ${order.id} оброблено`;
};Сучасні JavaScript-рушії часто виводять ім’я змінної processOrder навіть для анонімного виразу. Це називають автоматичним визначенням імені функції. Проте явне ім’я залишається корисним, особливо коли:
функція має інше ім’я змінної;
функцію передають як аргумент;
функція є частиною складної структури;
потрібно чітко позначити її призначення в налагоджувачі.
nameФункції мають властивість name, яка містить їхнє ім’я:
const anonymousFunction = function () {};
const namedFunction = function handleRequest() {};
console.log(anonymousFunction.name); // anonymousFunction
console.log(namedFunction.name); // handleRequestДля анонімного функціонального виразу рушій часто виводить ім’я зі змінної. Але явне ім’я функціонального виразу визначається безпосередньо:
const callback = function onDataReceived() {};
console.log(callback.name); // onDataReceivedВластивість name може бути корисною для діагностики, але не варто будувати основну логіку програми на конкретному значенні цієї властивості.
Іменування особливо корисне для callback-функцій, які не мають окремої зовнішньої змінної:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function doubleNumber(number) {
return number * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]У цьому прикладі doubleNumber:
документує призначення callback;
може відображатися в інструментах розробника;
допомагає ідентифікувати функцію під час налагодження.
Цей код також можна записати зі стрілковою функцією:
const doubled = numbers.map((number) => number * 2);Стрілкові функції не мають окремого синтаксису для іменованого функціонального виразу. Їхнє ім’я зазвичай виводиться з контексту, наприклад зі змінної або властивості.
IIFE — це функція, яку створюють і викликають одразу. Іменоване ім’я всередині IIFE можна використати для рекурсії або діагностики:
(function countdown(current) {
console.log(current);
if (current > 0) {
countdown(current - 1);
}
})(3);Результат:
3
2
1
0Ім’я countdown доступне лише всередині IIFE:
(function initialize() {
console.log("Ініціалізація");
})();
// initialize(); // ReferenceErrorconst sumTo = function (n) {
if (n <= 0) {
return 0;
}
return n + sumTo(n - 1);
};Перевага — короткий запис. Недолік — функція залежить від доступності та поточного значення sumTo.
const sumTo = function calculateSumTo(n) {
if (n <= 0) {
return 0;
}
return n + calculateSumTo(n - 1);
};Перевага — рекурсивний виклик прив’язаний до самої функції, а не до зовнішньої змінної.
Для простого коду обидва варіанти працюють. Іменований варіант часто краще підходить для функцій, які передають як значення, експортують або можуть переприсвоюватися.
const handler = function requestHandler() {
return "OK";
};
// requestHandler(); // ReferenceErrorПотрібно викликати функцію через зовнішню змінну:
handler();const execute = function saveData() {
return "Дані збережено";
};
console.log(execute.name); // saveDataУ цьому прикладі execute і saveData — різні імена з різними областями видимості.
// Це декларація функції:
function process() {}
// Це іменований функціональний вираз:
const processData = function process() {};У першому випадку process оголошується у зовнішній області видимості. У другому process є внутрішнім ім’ям функції, а зовнішнє ім’я — processData.
// showMessage(); // ReferenceError
const showMessage = function displayMessage() {
return "Повідомлення";
};Функціональний вираз не поводиться як декларація функції. Змінна showMessage існує в temporal dead zone до моменту ініціалізації.
const fn = function namedFn() {
// namedFn = anotherFunction;
};Внутрішнє ім’я слід розглядати як незмінне посилання на поточну функцію.
Вони особливо доречні, коли:
функція рекурсивна;
функцію передають як callback;
потрібен зрозумілий stack trace;
ім’я змінної не описує точне призначення функції;
функція може бути переприсвоєна через зовнішню змінну;
код має бути зручним для налагодження.
Для коротких простих callback-функцій анонімний вираз або стрілкова функція часто є достатніми:
const result = [1, 2, 3].map((value) => value * 2);Для складнішої логіки краще надати функції змістовне ім’я:
const result = [1, 2, 3].map(function doubleValue(value) {
return value * 2;
});Іменований функціональний вираз має ім’я після function.
Зовнішня змінна та внутрішнє ім’я функції можуть відрізнятися.
Внутрішнє ім’я доступне лише всередині функції.
Іменовані вирази зручні для рекурсії.
Рекурсивний виклик через внутрішнє ім’я не залежить від переприсвоєння зовнішньої змінної.
Явне ім’я покращує налагодження та читабельність stack trace.
Function Expression не має такого самого підняття, як Function Declaration.
Сучасні рушії можуть автоматично визначати ім’я анонімної функції, але явне ім’я краще документує складний код.