14 листопада 2023 р.

Об’єкт функції, NFE

Як ми вже знаємо, функція в 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, через які воно дається:

  1. Вона дозволяє функції посилатися на себе.
  2. Вона не доступна за межами функції.

Наприклад, функція sayHi нижче викликає себе знову "Гість" якщо who не надається:

let sayHi = function func(who) {
  if (who) {
    alert(`Привіт, ${who}`);
  } else {
    func("Гість"); // використовує func для повторного виклику
  }
};

sayHi(); // Привіт, Гість

// Але це не буде працювати:
func(); // Помилка, func не оголошена (недоступна за межами функції)

Чому ми використовуємо func? Можливо, просто використовувати sayHi для вкладеного виклику?

Насправді в більшості випадків ми можемо це зробити: