Як ми вже знаємо, функція в JavaScript – це значення.
Кожне значення в JavaScript має тип. Який тип функції?
У JavaScript, функції є об’єктами.
Ви можете уявити собі функції, як “об’єкти, що можна викликати, та які можуть виконувати якісь дії”. Ми можемо не тільки викликати їх, але й ставитися до них як до об’єктів: додавати/видаляти властивості, передавати за посиланням тощо.
Властивість “name”
Функціональні об’єкти містять деякі зручні властивості.
Наприклад, назва функції доступна як властивість “name”:
function sayHi() {
alert("Привіт");
}
alert(sayHi.name); // sayHi
Що доволі смішно, логіка присвоєння “name” досить розумна. Вона працює так, що призначає правильне ім’я функції, навіть якщо функція була створена без імені, а потім була негайно призначена:
let sayHi = function() {
alert("Привіт");
};
alert(sayHi.name); // sayHi (є ім’я!)
Це також працює, якщо призначення виконується за допомогою значення за замовчуванням:
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi (працює!)
}
f();
У специфікації ця ознака називається “контекстне ім’я”. Якщо функція не надає власне ім’я, то в присвоєнні воно з’являється з контексту.
Методи об’єктів також мають назви:
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
Проте тут немає ніякої магії. Є випадки, коли немає жодного способу з’ясувати правильну назву. У цьому випадку ім’я назви порожнє, як тут:
// функція створена всередині масиву
let arr = [function() {}];
alert( arr[0].name ); // <порожній рядок>
// рушій JavaScript не має можливості налаштувати правильну назву, тому в цьому випадку немає жодного значення
На практиці, однак, більшість функцій мають назву.
Властивість “length”
Існує ще одна вбудована властивість “length”, яка повертає кількість параметрів функції, наприклад:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2
В останньому випадку ми бачимо, що параметри, які було зібрано rest оператором, не підраховуються.
Властивість length
іноді використовується для інтроспекції у функціях, які працюють з іншими функціями.
Наприклад, у коді нижче функція ask
приймає як аргумент запитання question
та довільну кількість функцій-оброблювачів відповіді handler
.
Після того, як користувач надає відповідь, функція викликає оброблювачі. Ми можемо передати два типи обробників:
- функція без аргументів, яка лише викликається, коли користувач дає позитивну відповідь.
- функція з аргументами, яка називається в будь-якому випадку, і повертає відповідь.
Щоб викликати handler
правильно, ми розглядаємо властивість handler.length
.
Ідея полягає в тому, що у нас є простий, синтаксис обробника без аргументів для позитивних випадків (найчастіший варіант), але також підтримуються універсальні обробники:
function ask(question, ...handlers) {
let isYes = confirm(question);
for(let handler of handlers) {
if (handler.length == 0) {
if (isYes) handler();
} else {
handler(isYes);
}
}
}
// Для позитивної відповіді, обидва обробники викликаються
// для негативної відповіді, тільки другий
ask("Запитання?", () => alert('Ти сказав так'), result => alert(result));
Це конкретний випадок так званого поліморфізму – обробка аргументів по-різному залежно від їх типу або, у нашому випадку залежно від length
. Ця ідея використовується в бібліотеках JavaScript.
Кастомні властивості
Ми також можемо додати власні властивості.
Тут ми додаємо властивість counter
для відстеження загальної кількості викликів:
function sayHi() {
alert("Привіт");
// давайте порахувати, скільки викликів функції ми зробили
sayHi.counter++;
}
sayHi.counter = 0; // початкове значення
sayHi(); // Привіт
sayHi(); // Привіт
alert( `Викликана ${sayHi.counter} рази` ); // Викликана 2 рази
Властивість, присвоєна функції, як sayhi.counter = 0
не визначає локальну змінну counter
всередині цієї функції. Іншими словами, властивість counter
та змінна let counter
є двома незв’язаними речами.
Ми можемо використовувати функцію як об’єкт, зберігати властивості у ньому, але це не впливатиме на її виконання. Змінні – це не властивості функції і навпаки. Це два паралельні світи.
Властивості функцій можуть іноді замінити замикання. Наприклад, ми можемо переписати приклад функції лічильника з розділу Область видимості змінної, замикання використовуючи властивість функції:
function makeCounter() {
// замість:
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
Зараз count
зберігається в функції безпосередньо, а не у зовнішньому лексичному середовищі.
Це краще або гірше, ніж використання замикання?
Основна відмінність полягає в тому, що якщо значення count
живе в зовнішній змінній, то зовнішній код не може отримати доступ до нього.Тільки вкладені функції можуть змінювати його. А якщо це значення присвоєно як властивість функції, то ми можемо отримати до нього доступ:
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
counter.count = 10;
alert( counter() ); // 10
Таким чином, вибір реалізації залежить від наших цілей.
Named Function Expression
Named Function Expression, або NFE – це термін для Function Expressions, у якого є назва.
Наприклад, об’явімо звичайний Function Expression:
let sayHi = function(who) {
alert(`Привіт, ${who}`);
};
І додайте до нього назву:
let sayHi = function func(who) {
alert(`Привіт, ${who}`);
};
Чого ми досягли тут? Яка мета додаткової назви "func"
?
Спочатку відзначимо, що у нас ще є Function Expression. Додавання назви "func"
після function
не робить оголошення функції у вигляді Functional Declaration, оскільки функція все є частиною виразу присвоєння.
Додавання такої назви нічого не порушує.
Функція все ще доступна як sayHi()
:
let sayHi = function func(who) {
alert(`Привіт, ${who}`);
};
sayHi("Іван"); // Привіт, Іван
Є дві важливі особливості назви func
, через які воно дається:
- Вона дозволяє функції посилатися на себе.
- Вона не доступна за межами функції.
Наприклад, функція sayHi
нижче викликає себе знову "Гість"
якщо who
не надається:
let sayHi = function func(who) {
if (who) {
alert(`Привіт, ${who}`);
} else {
func("Гість"); // використовує func для повторного виклику
}
};
sayHi(); // Привіт, Гість
// Але це не буде працювати:
func(); // Помилка, func не оголошена (недоступна за межами функції)
Чому ми використовуємо func
? Можливо, просто використовувати sayHi
для вкладеного виклику?
Насправді в більшості випадків ми можемо це зробити:
let sayHi = function(who) {
if (who) {
alert(`Привіт, ${who}`);
} else {
sayHi("Гість");
}
};
Проблема з цим кодом полягає в тому, що sayHi
може змінюватися у зовнішньому коді. Якщо функція буде присвоєна іншій змінній, код почне давати помилки:
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest"); // Помилка: sayHi не є функцією
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Помилка, вкладений виклик sayHi більше не працює!
Це відбувається тому, що функція приймає sayHi
з його зовнішнього лексичного середовища. Там немає місцевого sayHi
, тому використовується зовнішня змінна. І в момент виклику зовнішній sayHi
є null
.
Необов’язкове ім’я, яке ми можемо ввести в Function Expression, призначене для розв’язання цих проблем.
Використовуймо це, щоб виправити наш код:
let sayHi = function func(who) {
if (who) {
alert(`Привіт, ${who}`);
} else {
func("Гість"); // Тепер все добре
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Привіт, Гість (вкладений виклик виконується)
Тепер це працює, тому що назва "func"
– локальне і знаходиться в середині функції. Воно не береться ззовні (і не доступно звідти). Специфікація гарантує, що воно завжди посилається на поточну функцію.
Зовнішній код все ще має свою змінну sayHi
або welcome
. А func
– це “внутрішнє ім’я функції”, яким функція може надійно викликати себе зсередини.
Функціональність з “внутрішньою назвою”, що описана вище, доступна лише для Function Expression, а не для Function Declaration. Для Function Declaration немає синтаксису для додавання “внутрішньої” назви.
Іноді, коли нам потрібна надійна внутрішня назва, це причина перезаписати Function Declaration на Named Function Expression.
Підсумки
Функції є об’єктами.
Їх властивості:
name
– назва функції. Зазвичай береться з оголошення функції, але якщо немає, JavaScript намагається здогадатися з контексту (наприклад, з присвоєння).length
– кількість аргументів в оголошенні функції. Параметри, що зібрані за допомогою rest оператора, не підраховуються.
Якщо функція оголошується як Function Expression (не в основному потоці коду), і має власну назву, то це називається Named Function Expression. Назва може бути використана всередині функції, щоб посилатися на саму себе, для рекурсійних викликів та ін…
Також функції можуть нести додаткові властивості. Багато відомих бібліотек JavaScript активно використовують цю властивість функції.
Вони створюють “головну” функцію і додають багато інших “допоміжних” функцій до неї. Наприклад, бібліотека jQuery створює функцію, що називається $
. Бібліотека lodash створює функцію _
, а потім додає _.clone
, _.keyBy
та інші властивості до неї (див. документацію, що дізнатися більше). Власне, вони роблять це, щоб зменшити своє забруднення глобального простору імен, так що одна бібліотека дає лише одну глобальну змінну. Це зменшує можливість конфліктів імен.
Отже, функція може робити корисну роботу сама по собі, а також нести купу інших функцій у властивостях.