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 для вкладеного виклику?

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

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 Declaration

Функціональність з “внутрішньою назвою”, що описана вище, доступна лише для 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 та інші властивості до неї (див. документацію, що дізнатися більше). Власне, вони роблять це, щоб зменшити своє забруднення глобального простору імен, так що одна бібліотека дає лише одну глобальну змінну. Це зменшує можливість конфліктів імен.

Отже, функція може робити корисну роботу сама по собі, а також нести купу інших функцій у властивостях.

Завдання

важливість: 5

Змініть код makeCounter() так, щоб лічильник міг також зменшити та встановити рахунок:

  • counter() повинен повернути наступний рахунок (як раніше).
  • counter.set(value) повинен встановити лічильник в значення value.
  • counter.decrease() повинен зменшити лічильник на 1.

Див. код пісочниці з повним прикладом використання.

P.S. Ви можете використовувати або замикання, або властивість функції, щоб зберегти поточний рахунок. Або напишіть обидва варіанти.

Відкрити пісочницю з тестами.

В рішенні використовується count у локальній змінній, але методи додавання записуються прямо в counter. Вони поділяють таке ж зовнішнє лексичне середовище, а також можуть отримати доступ до поточного count.

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

Відкрити рішення із тестами в пісочниці.

важливість: 2

Напишіть функцію sum, яка б працювала так:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

P.S. Підказка: вам може знадобитися налаштувати кастомний об’єкт, щоб конвертувати примітиви для вашої функції.

Відкрити пісочницю з тестами.

  1. Загалом, для того щоб все працювало хоч як-небудь, результат sum повинен бути функцією.
  2. Ця функція повинна зберігати в пам’яті поточне значення між викликами.
  3. Згідно з завданням, функція повинна стати числом, коли використовується в ==. Функції – це об’єкти, тому перетворення відбувається як описано в розділі Перетворення об’єктів в примітиви, і ми можемо надати власний метод, який повертає номер.

Тепер код:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

Зверніть увагу, що функція sum фактично працює лише раз. Вона повертає функцію f.

Потім, на кожному наступному виклику, f додає свій параметр до суми currentSum, і повертає себе.

В останньому рядку f немає ніякої рекурсії.

Ось як рекурсія виглядає:

function f(b) {
  currentSum += b;
  return f(); // <-- рекурсивний виклик
}

А в нашому випадку ми просто повертаємо цю функцію, не викликаючи її:

function f(b) {
  currentSum += b;
  return f; // <-- не викликає себе, повертає себе
}

Ця функція f буде використовуватися в наступному виклику і знову поверне себе стільки разів, скільки буде потрібно. Потім, при використанні функції як числа або рядка – метод toString поверне currentSum. Тут ми також можемо використовувати Symbol.toPrimitive або valueOf для конверсії.

Відкрити рішення із тестами в пісочниці.

Навчальна карта