Функції

Прокрутити вниз

Функції у JavaScript

Курси JavaScript для дітей один із найпростіших способів почати кар’єру в IT. Функції – ключова концепція JavaScript. Головна важлива відмінність цієї мови – це першокласна підтримка функцій (functions as first-class citizen). Будь-яка функція це об’єкт, і відповідно нею можна керувати як об’єктом, зокрема:

  • передавати як аргумент і повертати як результат при виклику інших функцій (функцій вищого порядку);
  • створювати анонімно і надавати як значень змінних чи властивостей об’єктів.

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

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

Для розуміння роботи функцій головне мати уявлення про такі моменти:

  • способи оголошення
  • способи виклику
  • параметри та аргументи виклику (arguments)
  • область даних (Scope) та замикання (Closures)
  • об’єкт прив’язки (this)
  • значення, що повертається (return)
  • винятки (throw)
  • використання як конструктор об’єктів
  • збирач сміття (garbage collector)

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

    Функції типу “function declaration statement”

Оголошення функції (function definition, або function declaration, або function statement) складається з ключового слова function і наступних блоків:

  • Назва функції.
  • Список параметрів (приймаються функцією) укладених у круглі дужки та розподілених комами.
  • Інструкції, які будуть виконані після виклику функції, укладають фігурні дужки { }.

Наприклад, наступний код оголошує просту функцію з ім’ям square:

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

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

return number * number;

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

Якщо ви передасте об’єкт як параметр (не примітив, наприклад, масив або об’єкти, що визначаються користувачем), і функція змінить властивість переданого в неї об’єкта, ця зміна буде видно і поза функцією, як показано в наступному прикладі:

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

var mycar = {make: ‘Honda’, model: ‘Accord’, year: 1998};
var x, y;

x = mycar.make; // x набуває значення “Honda”

myFunc(mycar);
y = mycar.make; // y отримує значення “Toyota”
// (Властивість було змінено функцією)

Функції виду “function definition expression”

Функція виду “function declaration statement” за синтаксисом є інструкцією (statement), ще функція може бути виду “function definition expression”. Така функція може бути анонімною, тобто без імені. Наприклад, функція square може бути викликана так:

var square = function(number) { return number * number; };
var x = square (4); // x набуває значення 16

Водночас ім’я може бути й присвоєно для виклику самої себе всередині самої функції та для відладчика (debugger) для ідентифікованих функцій у стек-треках (stack traces; “trace” – “слід” / “відбиток”).

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

console.log(factorial(3));

Функції виду “function definition expresion” зручні, коли функція передається аргументом іншої функції. Наступний приклад показує функцію map, яка має отримати функцію першим аргументом та масив другим.

function map(f, a) {
var result = [], // Create a new Array
i;
for (i = 0; i != a.length; i++)
result[i] = f(a[i]);
return result;
}

 function definition expression

У наступному коді наша функція приймає функцію, яка є function definition expression, і виконує його для кожного елемента прийнятого масиву другим аргументом.

function map(f, a) {
var result = []; // Create a new Array
var i; // Declare variable
for (i = 0; i != a.length; i++)
result[i] = f(a[i]);
return result;
}
var f = function(x) {
return x * x * x;
}
var numbers = [0, 1, 2, 5, 10];
var cube = map (f, numbers);
console.log(cube);

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

У JavaScript функція може бути оголошена за умови.

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

var myFunc;
if (num === 0) {
myFunc = function(theObject) {
theObject.make = ‘Toyota’;
}
}

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

Метод – це функція, що є властивістю об’єкта. Дізнатися більше про об’єкти та методи можна за допомогою курсів JavaScript для дітей.

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

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

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

square(5);

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

Функції можуть бути в області видимості, коли вони вже визначені, але функції виду “function declaration statment” можуть бути підняті (підняття —hoisting), також як у цьому прикладі:

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

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

Примітка: Це працює тільки тоді, коли оголошенні функції використовує вищезгаданий синтаксис (тобто function funcName(){}). Код нижче не працюватиме. Мається на увазі те, що підвищення функції працює тільки з function declaration і не працює з function expression.

console.log(square); // Square піднято зі значенням undefined.
console.log(square(5)); // TypeError: square is not a function
var square = function(n) {
return n*n;
}

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

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

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

Потім ви можете обчислити факторіали від одного до п’яти наступним чином:

var a, b, c, d, e;
a = factorial(1); // a gets the value 1
b = factorial(2); // b gets the value 2
c = factorial(3); // c gets the value 6
d = factorial(4); // d gets the value 24
e = factorial (5); // e gets the value 120

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

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

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

// Наступні змінні оголошені у глобальному scope
var num1 = 20,
num2 = 3,
name = ‘Chamahk’;

// Ця функція оголошена у глобальному scope
function multiply() {
return num1*num2;
}

multiply(); // Поверне 60

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

function add() {
return name + ‘scored’ + (num1 + num2);
}

return add();
}

getScore(); // поверне “Chamahk scored 5”

Scope та стек функції

Рекурсія

Функція може викликати себе. Три способи такого виклику:

  1. на ім’я функції
  2. arguments.callee
  3. за змінною, яка посилається на функцію

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

var foo = function bar() {
// statements go here
};

Всередині функції (function body) всі наступні еквівалентні виклики:

bar()
arguments.callee()
foo()

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

var x = 0;
while (x < 10) { // “x < 10” – це умова для циклу
// do stuff
x++;
}

можна було змінити на рекурсивну функцію та викликом цієї функції:

function loop(x) {
if (x >= 10) // “x >= 10” – це умова для кінця виконання (теж саме, що “!(x < 10)”)
return;
// робити щось
loop(x + 1); // Рекурсійний виклик
}
loop(0);

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

function walkTree(node) {
if (node ​​== null) //
return;
// щось робимо з елементами
for (var i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i]);
}
}

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

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

Поведінку стека можна побачити в наступному прикладі:

function foo(i) {
if (i < 0)
return;
console.log(‘begin:’ + i);
foo(i – 1);
console.log(‘end:’ + i);
}
foo(3);

// Output:

// begin: 3
// begin: 2
// begin: 1
// begin: 0
// end: 0
// end: 1
// end: 2
// end: 3

Вкладені функції (nested functions) та замикання (closures)

Можна вкласти одну функцію в іншу. Вкладена функція (nested function; inner) приватна (private) і вона поміщена в іншу функцію (outer). Так утворюється замикання (closure). Closure – це вираз (зазвичай функція), який може мати вільні змінні разом із середовищем, яке пов’язує ці змінні (що “закриває” (“close“) вираз).

Оскільки вкладена функція це closure, це означає, що вкладена функція може “успадкувати” (inherit) аргументи та змінні функції, в яку вона вкладена. Іншими словами, вкладена функція містить scope зовнішньої (“outer“) функції.

Підібємо підсумки:

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

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

function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // Повертає 13
b = addSquares (3, 4); // Повертає 25
c = addSquares (4, 5); // Повертає 41

Оскільки вкладена функція формує closure, ви можете викликати зовнішню функцію та вказати аргументи обох функцій (для outer та innner).

function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // Подумайте над цим: дайте мені функцію,
// який передай 3

result = fn_inside(5); // Повертає 8

result1 = outside(3)(5); // Повертає 8

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

Важливо: значення x збереглося, коли поверталося inside. Closure має зберігати аргументи та змінні у всьому scope. Оскільки кожен виклик надає потенційно різні аргументи, створюється новий closure для кожного виклику поза. Пам’ять може бути очищена тільки тоді, коли inside вже повернувся і більше не доступний.

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

Декілька рівнів вкладеності функцій (Multiply-nested functions)

Функції можна вкладати кілька разів, тобто. функція (A) зберігає функцію (B), яка зберігає в собі функцію (C). Обидві функції B і C формують closures, так B має доступ до змінних і аргументів A, і C має такий же доступ до B. На додаток, оскільки C має такий доступ до B, який має такий же доступ до A, C ще має такий ж доступ до A. Таким чином closures може зберігати у собі кілька scope; вони рекурсивно зберігають scope функцій, що містять його. Це називається chaining (chain – ланцюг; Чому названо “chaining” буде пояснено пізніше)

Розглянемо наступний приклад:

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 формує closure, що включає A, тобто. B має доступ до аргументів і змінних функцій A.
  2. Функція C формує closure, що включає B.
  3. Раз closure функції B включає A, то closure З також включає A, C має доступ до аргументів і змінних обох функцій B і A. Іншими словами, З зв’язує ланцюгом (chain) scopes функцій B і і

У зворотному порядку, однак, це не так. A не має доступ до змінних і аргументів C, тому що A не має такий доступ до B.Таким чином, C залишається приватним тільки для B.

Конфлікти назв (Name conflicts)

Коли два аргументи або змінних у scope у closure мають однакові імена, відбувається конфлікт імені (name conflict). Більше вкладений (more inner) scope має пріоритет, так самий вкладений scope має найвищий пріоритет, і навпаки. Це ланцюжок областей видимості (scope chain). Найпершою ланкою є найглибший scope, і навпаки. Розглянемо такі:

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

outside()(10); // Повертає 20 замість 10

Конфлікт імені стався в інструкції return x * 2 між параметром x функції inside і змінної x функції outside. Scope chain тут буде таким: {inside ==> outside ==> глобальний об’єкт (global object)}. Отже, x функції inside має більший пріоритет у порівнянні з outside, і нам повернулося 20 (= 10 * 2), а не 10 (= 5 * 2).

Замикання

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

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

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

Closure створюється, коли вкладена функція якось стала доступною в певному scope поза зовнішньою функцією.

var pet = function(name) { // Зовнішня функція оголосила змінну “name”
var getName = function() {
return name; // Вкладена функція має доступ до “name” зовнішньої функції
}
return getName; // Повертаємо вкладену функцію, тим самим зберігаючи доступ
// До неї для іншого scope
}
myPet = pet(‘Vivie’);

myPet(); // Повертається “Vivie”,
// т.к. навіть після виконання зовнішньої функції
// name зберігся для вкладеної функції

Складніший приклад представлений нижче.

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

var createPet = function(name) {
var sex;

return {
setName: function(newName) {
name=newName;
},

getName: function() {
return name;
},

getSex: function() {
return sex;
},

setSex: function(newSex) {
if(typeof newSex === ‘string’ && (newSex.toLowerCase() === ‘male’ ||
newSex.toLowerCase() === ‘female’)) {
sex=newSex;
}
}
}
}

var pet = createPet(‘Vivie’);
pet.getName(); // Vivie

pet.setName(‘Oliver’);
pet.setSex(‘male’);
pet.getSex(); // male
pet.getName(); // Oliver

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

var getCode = (function() {
var apiCode = 0;

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

getCode(); // Returns the apiCode

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

var createPet = function(name) { // Взаємна функція defines a variable called “name”.
return {
setName: function(name) { // Зазначена функція також defines a variable called “name”.
name = name; // How do we access the “name” defined by the outer function?
}
}
}

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

Об’єкт arguments функції є псевдомасивом. Всередині функції можна посилатися до аргументів таким чином:

arguments[i]

де i — це порядковий номер аргументу, що відраховується з 0. До першого аргументу, переданого функції, звертаються так arguments[0]. А отримати кількість всіх аргументів — arguments.length.

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

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

function myConcat(separator) {
var result = ”;
var i;

// iterate through arguments
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}

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

// повертає “red, orange, blue,”
myConcat(‘, ‘, ‘red’, ‘orange’, ‘blue’);

// повертає “elephant; giraffe; lion; cheetah;”
myConcat(‘; ‘, ‘elephant’, ‘giraffe’, ‘lion’, ‘cheetah’);

// Повертає “sage. basil. oregano. pepper. parsley.”
myConcat(‘.’, ‘sage’, ‘basil’, ‘oregano’, ‘pepper’, ‘parsley’);

Примітка: Arguments є псевдо-масивом, але не масивом. Це псевдо-масив, в якому є пронумеровані індекси та властивість length. Однак він не має всіх методів масивів.

Розгляньте об’єкт Function в JavaScript-довіднику для більшої інформації.

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

Починаючи з ECMAScript 2015 з’явилися два нові види параметрів: параметри за замовчуванням (default parameters) та залишкові параметри (rest parameters).

Параметри за замовчуванням (Default parameters)

У JavaScript параметри функції за замовчуванням мають значення undefined. Однак у деяких ситуаціях може бути корисним змінити значення за замовчуванням. У таких випадках default parameters можуть бути дуже доречними.

У минулому для цього було необхідно в тілі функції перевіряти значення параметрів на undefined і в позитивному випадку змінювати це значення на дефолтне (default). У наступному прикладі у разі, якщо при виклику не надали значення для b, то цим значенням стане undefined, тоді результатом обчислення a * b у функції multiply буде NaN. Однак у другому рядку ми зловимо це значення:

function multiply(a, b) {
b = typeof b !== ‘undefined’? b: 1;

return a*b;
}

multiply(5); // 5

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

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

multiply(5); // 5

Для більш детального розгляду ознайомтеся з параметрами за замовчуванням.

Залишкові параметри (Rest parameters)

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

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

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

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

Стрільні функції — функції виду “arrow function expression” (невірно fat arrow function) — мають укорочений синтаксис у порівнянні з function expression і лексично пов’язує значення this. Стрілецькі функції завжди анонімні. Подивіться також пост блогу hacks.mozilla.org ES6 In Depth: Arrow functions.

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

Коротші функції

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

var a = [
‘Hydrogen’,
‘Helium’,
‘Lithium’,
‘Beryllium’
];

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

console.log(a2); // logs [8, 6, 7, 9]

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

console.log(a3); // logs [8, 6, 7, 9]

Лексика this

До стрілочних функцій кожна нова функція визначала своє значення this (новий об’єкт у разі конструктора, undefined в strict mode, контекстний об’єкт, якщо функція викликана як метод об’єкта, і т.д.). Це виявилося дратівливим з погляду об’єктно-орієнтованого стилю програмування.

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

setInterval(function growUp() {
// Без strict mode функція growUp() визначає ‘this’
// як global object, який відрізняється від ‘this’
// Визначеного конструктором Person().
this.age++;
}, 1000);
}

var p = new Person();

У ECMAScript 3/5 цю проблему було виправлено шляхом присвоєння значення this змінної, яку можна було б замкнути.

function Person() {
var self = this; // Деякі вибирають ‘that’ замість ‘self’.
// Виберіть щось одне і будьте послідовні.
self.age = 0;

setInterval(function growUp() {
// The callback refers to the ‘self’ variable of which
// the value is the expected object.
self.age++;
}, 1000);
}

Альтернативою може бути пов’язана функція (bound function), з якою можна правильно вручну визначити значення this для функції growUp ().

В arrow function значенням this є оточуючий його контекст, так наступний код працює очікувано:

function Person() {
this.age = 0;

setInterval(() => {
this.age++; // | належним чином посилається на об’єкт Person
}, 1000);
}

var p = new Person();

Курси JavaScript для дітей перший крок стати успішним спеціалістом в IT сфері

Після прочитання статті перевірте свої знання, пройшовши тест. Сертифікат ви зможете обміняти на подарунки адміністратора школи. Уроки IT школи Gomother проходять онлайн та в офісі на м. Академмістечко, м. Житомирська. Бажаємо успіхів у проходженні тесту!