Функції

Функції – серед основних цеглинок, з котрих складається JavaScript. Функція в JavaScript подібна до процедури: низка інструкцій, котрі виконують завдання чи обчислюють значення; але щоб процедура вважалася функцією, вона повинна приймати певні значення на вхід і повертати виведення, щоб існував певний очевидний зв'язок між введенням та виведенням. Щоб використати функцію, її треба визначити десь в області видимості, з якої її буде викликано.

Дивіться подробиці також у вичерпному довідковому розділі про функції JavaScript.

Визначення функцій

Оголошення функцій

Визначення функції (котре також зветься оголошенням функції, або ж функційною інструкцією) складається з ключового слова function, після якого:

  • Ім'я функції.
  • Список параметрів функції, оточений дужками й розділений комами.
  • Інструкції JavaScript, котрі визначають функцію, оточені фігурними дужками – { /* … */ }.

Наприклад, наступний код визначає просту функцію, що зветься square:

function square(number) {
  return number * number;
}

Функція square приймає один параметр, що зветься number. Вона складається з однієї інструкції, котра каже повернути параметр функції (тобто number), помножений на себе. Інструкція return задає значення, повернене функцією, тобто number * number.

Параметри по суті передаються до функцій за значенням — тож якщо код всередині тіла функції присвоює параметрові, переданому в функцію, геть нове значення, то такі зміни не поширюються глобально чи в коді, котрий викликав цю функцію.

Коли об'єкт передається як параметр, то якщо функція змінює його властивості, то зміни помітні поза функцією, як показано в наступному прикладі:

function myFunc(theObject) {
  theObject.make = "Toyota";
}

const myCar = {
  make: "Honda",
  model: "Accord",
  year: 1998,
};

console.log(myCar.make); // "Honda"

// властивість make змінена функцією
myFunc(myCar);
console.log(myCar.make); // "Toyota"

Коли як параметр передається масив, то якщо функція змінює будь-яке зі значень масиву, то такі зміни помітні поза функцією, як показано в наступному прикладі:

function myFunc(theArr) {
  theArr[0] = 30;
}

const arr = [45];

console.log(arr[0]); // 45
myFunc(arr);
console.log(arr[0]); // 30

Вирази функцій

Коли оголошення функції з боку синтаксису є інструкцією, то функції також можна створити за допомогою виразів функцій.

Така функція може бути анонімною; вона не обов'язково повинна мати ім'я. Наприклад, функція square могла б бути визначена так:

const square = function (number) {
  return number * number;
};

console.log(square(4)); // 16

Проте ім'я може бути надано в виразі функції. Надання імені дає функції змогу посилатися на саму себе, а також полегшує впізнання функції трасуванні стека зневаджувача:

const factorial = function fac(n) {
  return n < 2 ? 1 : n * fac(n - 1);
};

console.log(factorial(3)); // 6

Вирази функцій зручні при передачі функції як аргументу в іншу функцію. Наступний приклад демонструє функцію map, котра повинна отримати функцію як перший аргумент і масив як другий аргумент:

function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}

В наступному коді map отримує функцію, визначену виразом функції, та виконує її для кожного елемента масиву, отриманого як другий аргумент:

function map(f, a) {
  const result = new Array(a.length);
  for (let i = 0; i < a.length; i++) {
    result[i] = f(a[i]);
  }
  return result;
}

const cube = function (x) {
  return x * x * x;
};

const numbers = [0, 1, 2, 5, 10];
console.log(map(cube, numbers)); // [0, 1, 8, 125, 1000]

Функція повертає: [0, 1, 8, 125, 1000].

У JavaScript функція може бути визначена на основі умови. Наприклад, наступне означення функції визначає myFunc лише за умови, що num дорівнює 0:

let myFunc;
if (num === 0) {
  myFunc = function (theObject) {
    theObject.make = "Toyota";
  };
}

На додачу до визначення функцій, як це описано вище, можна використати конструктор Function для створення функцій з рядків під час виконання, подібно до eval().

Метод – це функція, що є властивістю об'єкта. Читайте більше про об'єкти та методи в Роботі з об'єктами.

Виклик функції

Визначення функції не виконує її. Визначення дає їй ім'я та порядок дій у випадку її виклику.

Виклик функції вже виконує задані дії, з використанням вказаних параметрів. Наприклад, якщо визначити функцію square, то викликати її можна буде отак:

square(5);

Інструкція вище викликає функцію з аргументом 5. Функція виконує свої інструкції й повертає значення 25.

Функції повинні бути доступні в області видимості, коли викликані, але оголошення функції може бути піднято (стояти в коді нижче власного виклику). Областю видимості оголошення функції є та функція, в якій перша була оголошена (або вся програма, якщо функцію оголошено на верхньому рівні).

Аргументи функцій не обмежені рядками й числами. У функції можна передавати цілі об'єкти. Функція showProps() (визначена в Роботі з об'єктами) є прикладом функції, котра приймає об'єкт за аргумент.

Функція може викликати сама себе. Наприклад, ось функція, що рекурсивно обчислює факторіали:

function factorial(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

Тепер можна обчислити факторіали від 1 до 5 ось так:

console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6
console.log(factorial(4)); // 24
console.log(factorial(5)); // 120

Є інші способи викликати функцію. Нерідко трапляються випадки, коли функцію треба викликати динамічно, або коли число аргументів функції може бути різне, або коли контекст, в якому викликається функція, треба встановлювати як певний об'єкт під час виконання програми.

Виявляється, функції самі є об'єктами – і ці об'єкти й собі мають методи. (Дивіться об'єкт Function.) Методи call() і apply() можуть бути використані для досягнення такої цілі.

Підняття функцій

Розгляньмо приклад нижче:

console.log(square(5)); // 25
function square(n) {
  return n * n;
}

Такий код виконається без жодних помилок, попри те, що функція square() викликана до власного оголошення. Так відбувається через те, що інтерпретатор JavaScript піднімає все оголошення функції нагору поточної області видимості, тож код вище рівносильний такому кодові:

// Усі оголошення функцій по суті спрацьовують нагорі області видимості
function square(n) {
  return n * n;
}
console.log(square(5)); // 25

Підняття функцій працює лише для оголошень функцій – але не виразів функцій. Наступний код не запрацює:

console.log(square(5)); // ReferenceError: Cannot access 'square' before initialization
const square = function (n) {
  return n * n;
};

Функційна область видимості

До змінних, визначених всередині функції, не можна звертатися нізвідки поза функцією, тому що змінна визначена лише в області видимості цієї функції. Проте функція може звертатися до всіх змінних та функцій, визначених всередині області видимості, в котрій визначена сама ця функція.

Інакше кажучи, функція, визначена в глобальній області видимості, може звертатися до всіх змінних, визначених в глобальній області. Функція, визначена всередині іншої функції, на додачу може звертатися до всіх змінних, визначених у такій батьківській функції, а також будь-яких інших змінних, до котрих може звертатися батьківська функція.

// Наступні змінні визначені в глобальній області видимості
const num1 = 20;
const num2 = 3;
const name = "Шевченко";

// Ця функція визначена в глобальній області видимості
function multiply() {
  return num1 * num2;
}

console.log(multiply()); // 60

// Приклад укладеної функції
function getScore() {
  const num1 = 2;
  const num2 = 3;

  function add() {
    return `${name} має рахунок ${num1 + num2}`;
  }

  return add();
}

console.log(getScore()); // "Шевченко має рахунок 5"

Область видимості та стек функції

Рекурсія

Функція може звертатися до себе й викликати себе. Є три способи, що дають функції змогу звернутися до самої себе:

  1. Ім'я функції
  2. arguments.callee
  3. Доступна в області видимості змінна, що посилається на функцію

Наприклад, розгляньмо наступне визначення функції:

const foo = function bar() {
  // тут інструкції
};

Всередині тіла функції наступні записи – рівносильні:

  1. bar()
  2. arguments.callee()
  3. foo()

Функція, що викликає сама себе, зветься рекурсивною функцією. У певному відношенні рекурсія аналогічна циклові. І рекурсія, і цикл виконують один і той же код декілька разів, обидва потребують умови (аби уникнути нескінченного виконання циклу, або в цьому випадку – радше нескінченної рекурсії).

Наприклад, розгляньмо наступний цикл:

let x = 0;
while (x < 10) {
  // "x < 10" – умова циклу
  // різні дії
  x++;
}

Його можна перетворити на оголошення рекурсивної функції з викликом цієї функції:

function loop(x) {
  // "x >= 10" – умова виходу (рівносильна "!(x < 10)")
  if (x >= 10) {
    return;
  }
  // різні дії
  loop(x + 1); // рекурсивний виклик
}
loop(0);

Проте частина алгоритмів не може бути простими циклами ітерації. Наприклад, отримати всі вузли деревної структури (як то DOM) легше шляхом рекурсії:

function walkTree(node) {
  if (node === null) {
    return;
  }
  // якісь дії з вузлом
  for (let i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}

У порівнянні з функцією loop, кожний рекурсивний виклик сам робить тут чимало рекурсивних викликів.

Є можливим перетворити будь-який рекурсивний алгоритм на нерекурсивний, але при цьому логіка нерідко стає набагато складнішою, і таке перетворення вимагає використання стека.

Насправді рекурсія сама використовує стек – стек функції. Стекоподібна логіка може спостерігатися в наступному прикладі:

function foo(i) {
  if (i < 0) {
    return;
  }
  console.log(`початок: ${i}`);
  foo(i - 1);
  console.log(`кінець: ${i}`);
}
foo(3);

// Виводить:

// початок: 3
// початок: 2
// початок: 1
// початок: 0
// кінець: 0
// кінець: 1
// кінець: 2
// кінець: 3

Вкладені функції та замикання

Функцію можна вкласти в іншу функцію. Вкладена (внутрішня) функція є приватною для зовнішньої функції.

Також це утворює замикання. Замикання – це вираз (найчастіше – функція), що може мати вільні змінні вкупі з середовищем, котре зв'язує ці змінні (це "замикає" вираз).

Оскільки вкладена функція є замиканням, це означає, що вкладена функція може "успадкувати" аргументи та змінні зовнішньої функції. Інакше кажучи, внутрішня функція містить область видимості зовнішньої функції.

Підсумовуючи:

  • До внутрішньої функції можуть звертатися лише інструкції зовнішньої функції.
  • Внутрішня функція утворює замикання: внутрішня функція може використовувати аргументи та змінні зовнішньої функції, а зовнішня функція – не може використовувати аргументи та змінні внутрішньої.

Наступний приклад демонструє вкладені функції:

function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}
console.log(addSquares(2, 3)); // 13
console.log(addSquares(3, 4)); // 25
console.log(addSquares(4, 5)); // 41

Оскільки внутрішня функція утворює замикання, можна викликати зовнішню функцію й задати аргументи як для зовнішньої, так і для внутрішньої:

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}

const fnInside = outside(3); // Уявляйте це так: дай мені функцію, котра додає 3 до того, що їй передадуть
console.log(fnInside(5)); // 8
console.log(outside(3)(5)); // 8

Збереження змінних

Зверніть увагу, як зберігається x, коли повертається inside. Замикання мусить зберігати аргументи й змінні в усіх областях видимості, на котрі посилається. Оскільки кожний виклик передає потенційно різні аргументи, нове замикання створюється при кожному виклику outside. Пам'ять може бути звільнена лише тоді, коли повернена inside більше не доступна.

В цьому немає відмінності щодо збереження посилань в інших об'єктах, але в цьому випадку така логіка є менш очевидною, бо посилання не встановлюються безпосередньо, і їх не можна дослідити.

Багаторівнева вкладеність

Функції можуть бути вкладені на багатьох рівнях. Наприклад:

  • Одна функція (A) містить другу функцію (B), котра своєю чергою містить третю функцію (C).
  • І функція B, і функція C тут утворюють замикання. Тож B може звертатися до A, а C – до B.
  • Крім цього, оскільки C може звертатися до B, котра може звертатися до A, C також може звертатися до A.

Таким чином, замикання можуть вміщати декілька областей видимості; вони рекурсивно вміщають області видимості функцій, котрі вміщають їх.

Наприклад:

function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // Виводить 6 (тобто 1 + 2 + 3)

В цьому прикладі C звертається до y з B та x з A.

Це можливо, тому що:

  1. B утворює замикання, що включає A (тобто B може звертатися до аргументів та змінних A).
  2. C утворює замикання, що включає B.
  3. Оскільки замикання C включає B, а замикання B включає A, то замикання C також включає A. Це означає, що C може звертатися до аргументів і змінних як B, так і A. Інакше кажучи, C утворює ланцюжок з областей видимості B й A, в такому порядку.

Проте навпаки це не працює. A не може звертатися до C, бо A не може звертатися до жодних аргументів чи змінних B, змінною якої є C. Таким чином, C залишається приватною лише для B.

Конфлікти імен

Коли два аргументи чи змінні в областях видимості замикання мають однакове ім'я, трапляється конфлікт імен. Області видимості глибшої вкладеності отримують пріоритет. Отже, найглибша область отримує найвищий пріоритет, а крайня зовнішня – найнижчий. Так працює ланцюжок областей видимості. Першою в ланцюжку стоїть найглибша область, а останньою – крайня зовнішня. Для прикладу:

function outside() {
  const x = 5;
  function inside(x) {
    return x * 2;
  }
  return inside;
}

console.log(outside()(10)); //  20 (а не 10)

Конфлікт імен трапляється в інструкції return x * 2, між параметром insidex, і змінною outsidex. Ланцюжок тут – inside => outside => глобальний об'єкт. Таким чином, x з inside отримує пріоритет над x з outside, і повертається 20 (x з inside), а не 10 (x з outside).

Замикання

Замикання – одна з найпотужніших можливостей JavaScript. JavaScript дає змогу вкладати функції одна в одну й надавати внутрішній функції повний доступ до усіх змінних та функцій, визначених всередині зовнішньої функції (та всіх інших змінних та функцій, до котрих зовнішня функція має доступ).

Проте зовнішня функція не має доступу до змінних та функцій, визначених всередині внутрішньої функції. Це породжує певного роду інкапсуляцію змінних внутрішньої функції.

Крім того, оскільки внутрішня функція має доступ до області видимості зовнішньої функції, змінні та функції, визначені в зовнішній функції, житимуть довше, ніж триватиме виконання цієї зовнішньої функції, якщо внутрішня функція зможе пережити зовнішню функцію. Замикання створюється, коли внутрішня функція якось стала доступною будь-якій області видимості поза зовнішньою функцією.

const pet = function (name) {
  // Зовнішня функція визначає змінну, що зветься "name"
  const getName = function () {
    // Внутрішня функція має доступ до змінної "name" зовнішньої функції
    return name;
  };
  return getName; // Повернути внутрішню функцію, таким чином відкриваючи її для зовнішніх областей видимості
};
const myPet = pet("Лазанья");

console.log(myPet()); // "Лазанья"

Усе може бути складніше, ніж у коді вище. Можна повернути цілий об'єкт, що містить методи для роботи з внутрішніми змінними зовнішніх функцій.

const createPet = function (name) {
  let sex;

  const pet = {
    // setName(newName) рівносильно setName: function (newName)
    // у цьому контексті
    setName(newName) {
      name = newName;
    },

    getName() {
      return name;
    },

    getSex() {
      return sex;
    },

    setSex(newSex) {
      if (
        typeof newSex === "string" &&
        (newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")
      ) {
        sex = newSex;
      }
    },
  };

  return pet;
};

const pet = createPet("Лазанья");
console.log(pet.getName()); // Лазанья

pet.setName("Блек");
pet.setSex("male");
console.log(pet.getSex()); // male
console.log(pet.getName()); // Блек

У коді вище змінна name зовнішньої функції доступна внутрішнім функціям, і немає іншого способу звернутися до внутрішніх змінних, окрім як за допомогою внутрішніх функцій. Внутрішні змінні внутрішніх функцій діють як безпечні сховища для зовнішніх аргументів та змінних. Вони зберігають "постійні" й "інкапсульовані" дані для роботи внутрішніх функцій. Функції навіть не обов'язково присвоювати змінним чи давати їм ім'я.

const getCode = (function () {
  const apiCode = "0]Eal(eh&2"; // Код, котрий сторонні не повинні мати змогу редагувати…

  return function () {
    return apiCode;
  };
})();

console.log(getCode()); // "0]Eal(eh&2"

[!NOTE] Існує декілька підводних каменів, які слід мати на увазі під час застосування замикань!

Якщо замкнена функція визначає змінну з таким само ім'ям, як у якоїсь змінної зовнішньої області, то не існує способу знову звернутися до змінної зовнішньої області видимості. (Змінна внутрішньої області "заміщує" зовнішню доти, доки програма не покине внутрішньої області видимості. Можна розуміти це явище як конфлікт імен.)

// Зовнішня функція визначає змінну на ім'я "name".
const createPet = function (name) {
  return {
    // Замкнена функція також визначає змінну на ім'я "name".
    setName(name) {
      name = name; // Як звернутися до "name", визначеної зовнішньою функцією?
    },
  };
};

Використання об'єкта arguments

Аргументи функції зберігаються в масивоподібному об'єкті. Всередині функції можна звертатися до переданих аргументів наступним чином:

arguments[i];

де i – порядковий номер аргументу, починаючи від 0. Тож першим аргументом, переданим у функцію, буде arguments[0]. Загальне число аргументів показує властивість arguments.length.

За допомогою об'єкта arguments можна викликати функцію з більшою кількістю аргументів, ніж вона приймає згідно з її формальним оголошенням. Це нерідко корисно, коли наперед невідомо, скільки аргументів буде передано в функцію. arguments.length можна використовувати для з'ясування фактичної кількості переданих у функцію аргументів, а потім звернутися до кожного аргументу за допомогою об'єкта arguments.

Для прикладу візьмімо функцію, що зчіплює докупи кілька рядків. Єдиний формальний аргумент функції – рядок, що задає символи, котрі стоятимуть між елементами зчеплення. Функція визначена так:

function myConcat(separator) {
  let result = ""; // ініціалізація списку
  // ітерація по arguments
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}

В цю функцію можна передати будь-яку кількість аргументів, і вона зчепить усі аргументи в рядковий "список":

console.log(myConcat(", ", "червоний", "помаранчевий", "синій"));
// "червоний, помаранчевий, синій, "

console.log(myConcat("; ", "слон", "жирафа", "лев", "гепард"));
// "слон; жирафа; лев; гепард; "

console.log(
  myConcat(". ", "шавлія", "базилік", "орегано", "перець", "петрушка"),
);
// "шавлія. базилік. орегано. перець. петрушка. "

[!NOTE] Змінна arguments – "масивоподібне" значення, але не масив. Її масивоподібність полягає в тому, що вона має пронумеровані властивості та властивість length. Проте вона не має усіх методів роботи з масивами.

Подробиці доступні на сторінці об'єкта Function в довідці JavaScript.

Параметри функції

Є два особливі різновиди синтаксису параметрів: усталені параметри й решта параметрів.

Усталені параметри

В JavaScript усталене значення параметрів функцій – undefined. Проте в деяких ситуаціях може бути корисним задати інше усталене значення. Це саме те, що роблять усталені параметри.

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

В наступному прикладі, коли жодне значення не задано як b, його значення буде undefined при обчисленні a*b, і виклик multiply природно повернув би NaN. Проте цьому запобігає другий рядок цього прикладу:

function multiply(a, b) {
  b = typeof b !== "undefined" ? b : 1;
  return a * b;
}

console.log(multiply(5)); // 5

З усталеними параметрами ручна перевірка в тілі функції більше не потрібна. Можна поставити 1 як усталене значення b в голові функції:

function multiply(a, b = 1) {
  return a * b;
}

console.log(multiply(5)); // 5

Більше подробиць – на сторінці довідки Усталені параметри.

Решта параметрів

Синтаксис решти параметрів дає змогу представити необмежену кількість аргументів як масив.

В наступному прикладі функція multiply використовує решту параметрів для збору аргументів від другого до їх кінця. Потім функція множить їх на перший аргумент.

function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x);
}

const arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

Стрілкові функції

Вираз стрілкової функції (також зветься товстою стрілкою, для розрізнення щодо гіпотетичного синтаксису -> у JavaScript майбутнього) має коротший синтаксис у порівнянні з виразами функцій і не має власних this, arguments, super і new.target. Стрілкові функції завжди є анонімними.

Два чинники повпливали на запровадження стрілкових функцій: коротший запис функцій та незв'язування this.

Коротший запис функцій

У деяких функційних патернах коротший запис функцій – вітається. Порівняйте:

const a = ["Гідроген", "Гелій", "Літій", "Берилій"];

const a2 = a.map(function (s) {
  return s.length;
});

console.log(a2); // [8, 5, 5, 7]

const a3 = a.map((s) => s.length);

console.log(a3); // [8, 5, 5, 7]

Немає окремого this

До стрілкових функцій кожна нова функція визначала власне значення this (новий об'єкт у випадку конструктора, undefined при виклику функції в суворому режимі, базовий об'єкт, якщо функція викликана як "метод об'єкта" тощо). Така логіка виявилась менш ніж ідеальною в умовах програмування об'єктноорієнтованого стилю.

function Person() {
  // Конструктор Person() визначає `this` як самого себе.
  this.age = 0;

  setInterval(function growUp() {
    // В несуворому режимі функція growUp() визначає `this`
    // як глобальний об'єкт, а це не те саме, що `this`,
    // визначене конструктором Person().
    this.age++;
  }, 1000);
}

const p = new Person();

В ECMAScript 3/5 цю проблему розв'язували, присвоюючи значення this змінній, над котрою могло відбутися замикання.

function Person() {
  const self = this; // Іноді замість `self` обирають `that`.
  // Оберіть щось одне й будьте послідовними.
  self.age = 0;

  setInterval(function growUp() {
    // Функція зворотного виклику звертається до змінної `self`,
    // чиїм значенням є очікуваний об'єкт.
    self.age++;
  }, 1000);
}

Інший варіант: можна було використати зв'язану функцію, щоб у функцію growUp() потрапило коректне значення this.

Стрілкова функція не має власного this; використовується значення this із контексту виконання навколо неї. Отже, в наступному коді this всередині функції, переданої в setInterval, матиме таке саме значення, що й this у функції навколо неї:

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // `this` коректно вказує на об'єкт особи
  }, 1000);
}

const p = new Person();