Функції
Функції — важлива частина будь–якої мови програмування. До ECMAScript 6, функції в JavaScript з моменту створення мови, не зазнавали значних змін. Це накопичувало проблеми та нюанси поведінки, які призводили до помилкок та потребували більше коду для виконання базових завдань.
Функції в ECMAScript 6 зробили великий крок уперед, беручи до уваги скарги та прохання JavaScript–розробників. Результатом стали численні покращення ECMAScript 5 функцій, що допоможуть уникнути помилок у програмуванні на JavaScript та зроблять його більш потужним.
Функції з параметрами за замовчуванням
Функції в JavaScript є унікальними тому, що дозволяють приймати будь-яку кількість параметрів, незалежно від того, яку кількість параметрів було оголошено під час визначення функції. Це дозволяє вам визначати функції, що можуть оперувати різною кількістю параметрів просто підставляючи значення за замовчуванням, коли вони не передані. Цей розділ розповідає про те, як параметри за замовчуванням працюють до та в ECMAScript 6 разом з важливою інформацією про об’єкт arguments
, використання виразів у якості параметрів та інші нюанси.
Імітування параметрів за замовчуванням у ECMAScript 5
У ECMAScript 5 та раніше ви, напевно, використовували такий шаблон для створення функції з параметрами за замовчуванням:
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
// решта функції
}
У цьому прикладі і timeout
, і callback
є насправді необов’язковими, тому що їм передаються значення за замовчуваннями, якщо вони не вказані. Логічний оператор AБО (||
) завжди повертає другий оперант, якщо перший є хибним. Оскільки вказані параметри не вказані, їм буде встановелене значення undefined
, таким чином оператор логічного АБО часто використовується для встановлення пропущеним параметрам значень за замовчуванням. Однак, у такому підході є недолік: у тому випадку, якщо timeout
матиме значення 0
, яке є валідним, воно буде замінене на 2000
, тому що 0
є хибою.
У цьому випадку, безпечнішою альтернативою первірки чи було передано аргумент є використання typeof
, як у цьому прикладі:
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function() {};
// решта функції
}
Цей підхід безпечніший, але він потребує багато зайвого коду для такої простої операції. Популярні JavaScript–бібліотеки часто використовують подібний патерн, оскільки цей підхід є загальним.
Параметри за замовчуванням у ECMAScript 6
ECMAScript 6 полегшує передачу значень параметрам за замовчуванням шляхом ініціалізації, яка відбувається, коли параметр не був переданий. Наприклад:
function makeRequest(url, timeout = 2000, callback = function() {}) {
// решта функції
}
Ця функція очікує, що лише перший параметр буде передаватись завжди. Інші два параметри мають значення за замовчуванням, що робить тіло функції меншим, оскільки вам не потрібно писати додатковий код для перевірки значень.
Коли makeRequest()
з усіма трьома параметрами, параметри за замовчуванням не будуть використовуватись. Наприклад:
// використовує значення timeout та callback за замовчуванням
makeRequest("/foo");
// використовує callback за замовчуванням
makeRequest("/foo", 500);
// не використовує значення за замовчуванням
makeRequest("/foo", 500, function(body) {
doSomething(body);
});
ECMAScript 6 розглядає url
як обов’язковий параметр, тому ми передаємо "/foo"
у всіх трьох викликах makeRequest()
. Два параметри зі значеннями за замовчуванням розглядаються як необов’язкові.
Можливо задавати значення за замовчуванням для будь–яких аргументів, враховуючи ті, які знаходяться у оголошенні функції перед аргументами без значень за замовчуванням. Наприклад, такий код працює як і очікується:
function makeRequest(url, timeout = 2000, callback) {
// решта функції
}
У цьому випадку значення timeout
за замовчування використовуватиметься лише тоді, коли другий аргумент не переданий, або якщо в якості другого аргументу безпосередньо передати undefined
, як у цьому прикладі:
// використовує timeout за замовчуванням
makeRequest("/foo", undefined, function(body) {
doSomething(body);
});
// використовує значення timeout за замовчуванням
makeRequest("/foo");
// не використовує значення timeout за замовчуванням
makeRequest("/foo", null, function(body) {
doSomething(body);
});
У цьому випадку, значення null
сприймається як валідне і означає, що в третьому виклику makeRequest()
значення timeout
за замовчуванням не буде використовуватись.
Як параметри за замовчуванням впливають на об’єкт arguments
Просто запам’ятайте, що поведніка об’єкту arguments
відрізняється від звичної, якщо використовуються значення за замовчуванням. У нестрогому режимі (nonstrict mode) ECMAScript 5 об’єкт arguments
відображає зміни в іменованих параметрах функції. Нижче наведений код, який ілюструє як це працює:
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
Вивід:
true
true
true
true
У нестрогому режимі об’єкт arguments
завжди оновлюється, щоб відображати зміни в іменованих параметрах. Тобто, якщо для first
та second
присвоїти нові значення, arguments[0]
та arguments[1]
оновляться миттєво, тому порівняння ===
даватиме результат true
.
Однак строгий режим (strict mode) ECMAScript 5 усуває цю неочевидну властивість об’єкта arguments
. У строгому режимі, об’єкт arguments
не відображає зміни в іменованих параметрах. Нижче знову наведена функція mixArgs()
, але у строгому режимі:
function mixArgs(first, second) {
"use strict";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
Виклик mixArgs()
виведе:
true
true
false
false
Цього разу зміни first
та second
не вплинули на arguments
, тому вивід буде саме таким, яким ви його очікуєте.
Об’єкт arguments
у функціях, що використовують ECMAScript 6 параметри за замовчуванням, однак, завжди поводитимуться так, як вони поводяться у строгому режимі ECMAScript 5, незалежно від того, чи функція працює у строгому режимі, чи ні. Наявність параметрів за замовчуванням робить об’єкт arguments
незалежним від іменованих параметрів. Це тонка, але важлива деталь того, як об’єкт arguments
може бути використаний. Розгляньте наступне:
// не строгий режим
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");
Виведе:
1
true
false
false
false
У цьому прикладі, arguments.length
рівне 1 тому, що лише один аргумент було передано до mixArgs()
. Це також означає, що arguments[1]
рівний undefined
, що є очікованою поведінкою, коли лише один аргумент передається у функцію. Це також означає, що first
є рівним arguments[0]
. Зміна first
та second
не повпливає на arguments
. Така поведінка буде як у строгому, так і в нестрогому режимах, тож ви можете бути певні, що arguments
завжди відображатиме початковий стан виклику.
Вирази в параметрах за замовчуванням
Одним з найбільш цікавих нововведень параметрів за замовчуванням є те, що їхні значення не обов’язково мають бути примітивними. Ви можете, наприклад, викликати функцію, що повертатиме значення параметру, ось так:
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
Тут, якщо останній аргумент не переданий, викликається фукнція getValue()
, що повертає правильне значення за замовчуванням. Пам’ятайте, що getValue()
буде викликана лише тоді, коли add()
буде викликана без другого аргументу, а не тоді, коли оголошення функції буде оброблене інтерпретатором. Це означає, що якщо getValue()
була написана змінною, вона буде повертати різні значення. Для розуміння:
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7
У цьому прикладі, value
має значення п’ять і збільшується на одну одиницю щоразу, коли викликається getValue()
. Перший виклик add(1)
повертає 6, а другий виклик add(1)
повертає 7, тому що value
був збільшений на одиницю. Оскільки значення second
за замовчуванням обчислюється під час виклику функції, можна змінити це значення у будь–який час.
W> Будьте обережні при використанні виклику фукнцій в якості параметрів за замовчуванням. Якщо ви забудете дужки, як ось second = getValue
у останньому прикладі, ви отримаєте посилання на функцію замість результату її виклику.
Така поведінка демонструє іншу цікаву особливість. Ви можете використовувати параметр за замовчуванням для наступного параметра. Ось приклад:
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
У цьому прикладі, параметр second
отримує значення first
за замовчуванням, що означає, що виклик фукнції з одним аргументом присвоїть обом параметрам однакове значення. Тому add(1, 1)
поверне 2 так само, як це робить add(1)
. Навіть більше, ви можете передати first
у функцію, щоб отримаи значення second
ось так:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
Цей приклад встановлює second
рівним значенню, яке було повернуте getValue(first)
, тому add(1, 1)
продовжує повертати 2, а add(1)
поверне 7 (1 + 6).
Можливість посилатись на параметри працює лише за умови звернення до попередніх аргументів, тому аргументи попереду не мають доступу до наступних аргументів. Наприклад:
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // викине помилку
Виклик add(1)
викине помилку, тому що second
визначений після first
і тому недоступний в якості значення за замовчуванням. Щоб зрозуміти що тут відбувається, важливо згадати що таке тимчасова мертва зона.
Тимчасова мертва зона параметрів за замовчуванням
Глава 1 ввела поняття тимчасової мертвої зони (ТМЗ) яка стосувалась let
та const
, втім, параметри за замовчуванням також мають ТМЗ, з якої параметри будуть недоступними. Подібно до let
–оголошення, кожен параметр створює новий ідентифікатор зв’язування, на який не можна посилатись до його ініціалізації, не спричинивши помилки. Ініціалізація параметра відбувається або тоді, коли функція викликається, або при передачі параметрам значення, або при використанні значень параметрів за замовчуванням.
Щоб дослідити ТМЗ параметрів за замовчуванням, розглянемо цей приклад з розділу «Вирази в параметрах за замовчуванням»:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
Виклики add(1, 1)
та add(1)
ефективно виконують присвоєння first
та second
значень за замовчуванням:
// JavaScript представлення виклику add(1, 1)
let first = 1;
let second = 1;
// JavaScript представлення виклику add(1)
let first = 1;
let second = getValue(first);
Коли функція add()
виконується вперше, зв’язування first
та second
додаються до специфічної для параметрів ТМЗ (схоже на те, як поводить себе let
). Тому хоча second
може бути ініціалізований зі значенням first
, оскільки first
у той момент буде вже ініціалізованим, але протилежне не є вірним. Тепер розглянемо змінену функцію add()
:
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // кидає помилку
Давайте розглянемо, що відбувається за лаштунками під час виклику add(1, 1)
та add(undefined, 1)
у цьому прикладі:
// JavaScript представлення виклику add(1, 1)
let first = 1;
let second = 1;
// JavaScript представлення виклику add(undefined, 1)
let first = second;
let second = 1;
У цьому випадку, виклик add(undefined, 1)
призводить до помилки, тому що second
ще не був ініціалізований тоді, коли ініціалізувався first
. У цьому випадку, second
знаходиться у ТМЗ, і тому будь–яке посилання на нього призведе до помилки. Схожу поведінку let
–оголошень було розглянуто у Главі 1.
I> Параметри функцій мають власну області видимості та власні ТМЗ, що відрізняються від області видимості функцій. Це означає, що значення параметрів за замовчуванням не можуть звертатись до будь–якої змінної, оголошеної всередині тіла функції.
Робота з неіменованими параметрами
Досі, приклади у цій главі були пов’язані лише з параметрами, які мали імена у виразі функції. Однак, функції JavaScript не обмежують кількість параметрів, які ми можемо передавати у функцію, кількістю параметрів, які вказані при заданні функції. Ви завжди можете передати менше або більше параметрів, ніж формально оголошено. Значення за замовчування допомагають тоді, коли функція може приймати менше параметрів, але ECMAScript 6 також пропонує вирішення проблеми з передачею більшої кількості параметрів, ніж вказано в оголошенні.
Неіменовані параметри в ECMAScript 5
Раніше JavaScript пропонував об’єкт arguments
для того, щоб працювати з усіма параметрами, які були передані у функцію, без необхдіності вказувати кожен параметр індивідуально. Об’єкт arguments
чудово працює у більшості випадків, проте цей об’єкт може бути занадто громіздким. Наприклад, дослідіть цей код, який оперує об’єктом arguments
:
function pick(object) {
let result = Object.create(null);
// починаючи з другого параметра
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "Understanding ECMAScript 6",
author: "Nicholas C. Zakas",
year: 2015
};
let bookData = pick(book, "author", "year");
console.log(bookData.author); // "Nicholas C. Zakas"
console.log(bookData.year); // 2015
Ця фукнція відтворює метод pick()
з бібліотеки Underscore.js, який повертає копію даного об’єкту з деякою підмножиною властивостей оригінального об’єкту. У цьому прикладі оголошується лише один аргумент і очікується, що він буде об’єктом, з якого будуть копіюватись властивості. Усі інші аргументи, які передаються — це імена властивостей, які мають бути копійованими до результату.
Є кілька речей, які потрібно зауважити у функції pick()
. По–перше, не зовсім очевидно, що функція може обробляти більше одного параметра. Ви могли б задати більше параметрів, але було б не достатньо очевидно, що фукнція може приймати будь–яку кількість параметрів. По–друге, оскільки перший параметр іменований та використовується безпосередньо, якщо ви звернете увагу на властивості, які мають бути скопійовані, то муситиме звертатись до об’єкта arguments
починаючи з індексу 1 замість індексу 0. Запам’ятовування відповідних індексів для arguments
не є складним, проте це ще одна річ, за якою потрібно стежити.
ECMAScript 6 вводить залишкові (rest) параметри, щоб вирішити цю проблему.
Залишкові (rest) параметри
Залишкові параметри (або rest–параметри) позначаються трьома крапками (...
) перед іменованим параметром. Цей іменований параметр стає масивом (Array
), що містить решту параметрів, які були передані функції. Наприклад, pick()
можна переписати з використанням залишкових параметрів ось так:
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
У цій версії функції, keys
— це залишковий параметр, який містить всі параметри, передані після object
(на відміну від arguments
, який містить всі параметри, враховуючи перший). Це означає, що ви без проблем можете ітеруватись по keys
від початку і до кінця. В якості бонусу, ви можете одразу помітити, що функція працює з будь–якою кількістю параметрів.
I> Залишкові параметри не впливають на властивість функції length
, яка вказує на кількість іменованих параметрів функції. Значення length
для pick()
у цьому прикладі є рівним 1, оскільки буде враховутись лише object
.
Обмеження залишкових параметрів
Є два обмеження пов’язані з залишковими параметрами. Перше обмеження: може бути лише один залишковий параметр і він має бути останнім. Для прикладу, такий код не працюватиме.
// Syntax error: Can't have a named parameter after rest parameters
function pick(object, ...keys, last) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
Тут, праметр last
слідує за залишковим параметром keys
, що спричинило б синтактичну помилку.
Друге обмеження — це те, що залишковий параметр не може використовуватись у сетерах об’єктних літералів. Це означає, що такий код також призведе до помилки:
let object = {
// Syntax error: Can't use rest param in setter
set name(...value) {
// якийсь код
}
};
Це обмеження має місце тому, що сетери об’єктних літералів обмежуються одним аргументом. Залишкові параметри, за визначенням, є нескінченною кількістю аргументів, тож вони не дозволені у цьому контексті.
Як залишкові параметри впливають на об’єкт arguments
Залишкові параметри покликані замінити arguments
в ECMAScript. Спочатку, ECMAScript 4 покінчив з arguments
та включав залишкові параметри, які дозволяли б передавати необмежену кількість аргументів у функцію. ECMAScript 4 не був прийнятим, але ця ідея була збережена та відтворена у ECMAScript 6, незважаючи на те, що arguments
не був видалений з мови.
Об’єкт arguments
працює разом із залишковими параметрами, відображаючи аргументи, які були передані до функції при виклику. Це ілюструє така програма:
function checkArgs(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args[0], arguments[0]);
console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
Виклик checkArgs()
виведе:
2
2
a a
b b
Об’єкт arguments
завжди коректно відображає параметри, що були передані до функції незалежно від використання залишкових параметрів.
Це все, що вам потрібно знати про залишкові параметри, щоб почати використовувати їх. Наступний розділ продовжить розповідь про оператор розкладу (spread), який є дуже схожим на залишкові параметри.
Розширені можливості конструктора Function
Конструктор Function
є рідковикористовуваною частиною JavaScript, яка дозволяє вам динамічно створювати нові функції. Аргументами конструктора є параметри для функції та тіло функції (всі аргументи у вигляді рядків). Ось приклад:
var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2
ECMAScript 6 доповнює Function
можливостями задання параметрів за замовчуванням та залишкових параметрів. Вам потрібно лише додати знак рівності між значенням та ім’ям параметра:
var add = new Function("first", "second = first",
"return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
У цьому прикладі параметру second
присвоюється значення first
, коли переданий лише один параметр. Синтаксис такий же, як і для функцій, що не використовують Function
.
Для залишкових параметрів, просто додайте ...
перед останнім параметром, як тут:
var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2)); // 1
Цей код створює функцію, яка використовує лише один залишковий параметр та повертає перший аргумент, який був переданий у функцію.
Доповнення параметрами за замовчуванням та залишковими параметрами прирівнює можливості Function
до можливостей декларативних форм оголошення функцій.
Оператор розкладу (spread)
Близьким до залишкових параметрів є оператор розкладу (spread). Залишкові параметри дозволяють вам вказати, як кілька незалежних аргументів можуть комбінуватись у масив, тоді, як оператор розкладу дозволяє вам задати як масив має розкласти свої елементи у окремі аргументи функції. Розгляньте метод Math.max()
, який приймає будь–яку кількість аргументів та повертає найбільше значення. Ось приклад використання цього методу:
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50
Коли ви працюєте з двома значеннями, як у цьому прикладі, Math.max()
використовувати легко. Два значення передаються і повертається найбільше. Але що, якщо ви працюєте з значеннями масиву, і вам потрібно знайти найбільше значення? Метод Math.max()
не дозволяє вам передавати масив, тому в ECMAScript 5 та раніше ви би застрягли шукаючи значення самостійно або використовували б apply()
як ось тут:
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100
Це рішення працює, але використання apply()
в такому вигляді вносить деяку плутанину. Це виглядає, наче навмисне заплутування коду додатковим синтаксисом.
Оператор розкладу ECMAScript 6 вирішує цю проблему дуже легко. Замість виклику apply()
, ви можете передати масив у Math.max()
напряму, додавши попереду ...
, як і у випадку з залишковими параметрами. Рушій JavaScript розкладе масив у окремі аргументи та передасть їх у функцію, ось так:
let values = [25, 50, 75, 100]
// еквівалент для
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100
Тепер виклик Math.max()
виглядає більш звично та уникає складності, пов’язаної з заданням this
–зв’язування (першого аргументу Math.max.apply()
у попередньому прикладі) для простої математичної операції.
Ви можете змішувати та комбінувати оператор розкладу з іншими аргументами. Припустимо, вам потрібно, щоб найменшим числом, яке поверне Math.max()
був 0 (просто для випадку, якщо якесь число менше 0 закрадеться у масив). Ви можете передати аргумент окремо та продовжувати використовувати оператор розкладу для інших аргументів:
let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0
У цьому прикладі, останній аргумент, переданий до Math.max()
буде 0
, який був переданий після всіх інших аргументів, переданих з використанням оператору розкладу.
Оператор розкладу для передачі аргументів робить використання масивів у функцях набагато легшим. Ви знайдете зручну заміну для методу apply()
у більшості випадках.
На додачу до прикладів використання залишкових та параметрів за замовчуванням, в ECMAScript 6, ви можете також використовувати обидва типи у JavaScript–конструкторі Function
.
Властивість name в ECMAScript 6
Ідентифікування функцій в JavaScript може виявитись складним завданням, зважаючи на численні способи визначення функцій. Крім того, поширення анонімних функцій робить відлагоджування набагато складнішим, адже стек викликів є складним для розуміння. Виходячи з цих міркувань, ECMAScript 6 вводить властивість name
для всіх функцій.
Вибір відповідного імені
Всі функції в програмах на ECMAScript 6 будуть мати відповідні значення властивості name
. Щоб побачити це в дії, погляньте на приклад нижче, який демонструє фукнцію і функціональний вираз та виводить властивість name
для обох:
function doSomething() {
// ...
}
var doAnotherThing = function() {
// ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"
У цьому коді, doSomething()
має властивість name
, що рівна "doSomething"
, оскільки це оголошення функції. Вираз з анонімною функцією doAnotherThing()
має name
рівне "doAnotherThing"
, тому що таке ім’я змінної, якій він був присвоєний.
Особливі випадки властивості name
Відповідні імена для оголошень фукнцій та функціональних виразів знайти легко, але ECMAScript 6 іде далі, щоб впевнетись, що всі функції мають відповідні імена. Щоб зрозуміти це, розгляньте наступний приклад:
var doSomething = function doSomethingElse() {
// ...
};
var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
console.log(person.firstName.name); // "get firstName"
У цьому прикладі,doSomething.name
є "doSomethingElse"
оскільки функціональний вираз має власне ім’я, яке має пріоритет над змінною, якій ця функція була присвоєна. Властивість name
у person.sayName()
рівна "sayName"
, бо це значення було інтерпретоване з об’єктного літералу. Так само, person.firstName
насправді є функцією–гетером, тож її ім’я "get firstName"
, що вказує на цю відмінність. Функції–сетери так само позначаються через "set"
.
Також є кілька інших особливих випадків для імен функцій. Функції, створені з використанням bind()
, будуть мати імена з префіксованим "bound"
, а функції, створені конструктором Function
мають ім’я "anonymous"
:
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
name
зв’язаної функції буде завжди name
функції, що була зв’язана з префіксом "bound "
, тому зв’язана версія doSomething()
буде "bound doSomething"
.
Запам’ятайте, що значення name
для будь–якої функції не обов’язково посилається на змінну з таким самим ім’ям. Властивість name
не покликана допомагати у відлагодженні, тому неможливо використати значення name
для отримання посилання на функцію.
Роз’яснення подвійної ролі функцій
в ECMAScript 5 та раніше, функції можна було створити двома способами: з або без new
. При використанні new
, значення this
всередині функції було новим об’єктом і цей об’єкт повертався, так як це ілюстровано в цьому прикладі:
function Person(name) {
this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"
При створенні notAPerson
, виклик Person()
без new
повертає undefined
(та встановлює властивість name
глобального об’єкта у нестрогому режимі). В програмах на JavaScript прийнято, що Person
з великої літери — це єдиний індикатор того, що функція має бути викликана з використанням new
. Таке подвійне призначення функцій призводило до плутанини і тому зазнало деяких змін у ECMAScript 6.
JavaScript має два різних внутрішніх методи для функцій: [[Call]]
та [[Construct]]
. Коли функція викликається без new
, виконується метод [[Call]]
, який виконує тіло функцій так, як це описано в коді. Коли функція викликається з new
, тоді викликається [[Construct]]
. Метод [[Construct]]
відповідальний за створення нового об’єкта, і тоді виконання тіла функції з this
, встановленим для нового об’єкта. Функції, які мають метод [[Construct]]
називаються конструкторами.
I> Запам’ятайте, що не всі функції мають [[Construct]]
, тому не всі вони можуть бути викликані з new
. Arrow–функції, про які буде йтись у відповідному розділі, не мають методу [[Construct]]
.
Визначення того, як функція була викликана у ECMAScript 5
Найпопулярніший спосіб визначити, що функція була викликана з new
(а отже й з конструктором) у ECMAScript 5 — це використати instanceof
, для прикладу:
function Person(name) {
if (this instanceof Person) {
this.name = name; // використано new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // кидає помилку
Тут значення this
перевіряється, чи воно відповідає конструтору. Якщо воно відповідає, то виконання виконується так, як і слід. Якщо this
не відповідає Person
, тоді кидається помилка. Це працює тому, що метод [[Construct]]
створює новий екземпляр Person
та присвоює його в this
. На жаль, такий підхід недостатньо надійний, оскільки this
може відповідати Person
і без використання new
, наприклад:
function Person(name) {
if (this instanceof Person) {
this.name = name; // з використанням new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // працює!
Виклику Person.call()
передається змінна person
в якості першого аргументу, що означає this
встановлений в person
всередині функції Person
. Для функцій немає ніякого способу відрізнити це від виклику з new
.
Метавластивість new.target
Щоб вирішити цю проблему, ECMAScript 6 вводить метавластивість new.target
. Метавластивість — це властивість для необ’єктів, яка надає додаткову інформацію, яка стосується цільового об’єкта (як от new
). Коли метод [[Construct]]
функції викликаний, new.target
заповнюється цільовим об’єктом оператора new
. Цей цільовий об’єкт є зазвичай новоствореним екземпляром об’єкту, який стає this
всередині тіла функції. Якщо виконано [[Call]]
, тоді new.target
буде undefined
.
Ця нова метавластивість дозволяє вам безпечно перевіряти, чи функція була викликана з new
, простою перевіркою того, чи було встановлено new.target
є:
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name; // з використанням new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // помилка!
З використанням new.target
замість this instanceof Person
, конструктор Person
тепер кидатиме помилку при використанні без new
.
Ви також можете перевірити, чи new.target
було викликано з певним конструктором. Для прикладу:
function Person(name) {
if (typeof new.target === Person) {
this.name = name; // використовуючи new
} else {
throw new Error("You must use new with Person.")
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // помилка!
У цьому прикладі, new.target
мусить бути Person
для коректної роботи. Коли new AnotherPerson("Nicholas")
створюється, new.target
встановлюється в AnotherPerson
, тому наступний виклик Person.call(this, name)
кине помилку попри те, що new.target
буде встановлено.
W> Увага: використання new.target
поза функцією призведе до помилки.
Додаючи new.target
, ECMAScript 6 допомагає прояснити деякі незрозумілості, пов’язані з викликом функцій. Продовжуючи тему, ECMAScript 6 також вводить раніше незрозумілу частину мови — оголошення функцій всередині блоків.
Блочні функції
У ECMAScript 3 та раніше оголошення функції всередині блоку (блочні функції) технічно було синтаксичною помилкою, проте всі браузери продовжували підтримувати це. На жаль, кожен браузер, що дозволяв такий синтаксис, поводився по різному, тому було прийнято уникати оголошення функцій всередині блоків (найкращою альтернативою було використання функціональних виразів).
Намагаючись обмежети таку несумісну поведінку, строгий режим в ECMAScript 5 призводив до помилки при кожному оголошенні функції всередині блока:
"use strict";
if (true) {
// Кидає синтаксичну помилку у ES5, але не в ES6
function doSomething() {
// ...
}
}
У ECMAScript 5 такий код кидає синтаксичну помилку. У ECMAScript 6 функція doSomething()
розглядається як блочне оголошення і може бути доступною та викликаною всередині блока, в якому вона була оголошена. Наприклад:
"use strict";
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "undefined"
Блочні функції виринають на вершину блока, в якому вони були оголошені, тому typeof doSomething
повертає "function"
, не дивлячись на те, що ця інструкція знаходиться перед оголошенням функції. Як тільки блок if
закінчить виконання, doSomething()
більше не існуватиме.
Коли використовувати блочні функції
Блочні функції схожі на функціональні вирази з let
. За умови такого визначення, функції видаляються, як тільки хід виконання програми виходить з блока, у якому вони були оголошені. Основна відмінність у тому, що блочні функції виринають на вершину блока, у якому вони оголошені. Функціональні вирази, що використовують let
, не виринають, як це показано у цьому прикладі:
"use strict";
if (true) {
console.log(typeof doSomething); // кине помилку
let doSomething = function () {
// ...
}
doSomething();
}
console.log(typeof doSomething);
Тут виконання коду зупиниться, коли typeof doSomething
буде виконано, оскільки оператор let
ще не було виконано, залишивши doSomething()
у ТМЗ. Знаючи цю відмінність, ви можете обирати, де використовувати блочні функції, а де вирази з let
, в залежності від того, чи потрібне вам виринання, чи ні.
Блочні функції у нестрогому режимі
ECMAScript 6 також дозволяє блочні функції у нестрогому режимі, але з дещо іншою поведінкою. Замість виринання цього оголошення на вершину блока, вони будуть виринати на вершину функції або глобального оточення, в якому містяться. До прикладу:
// Поведінка ECMAScript 6
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "function"
У цьому прикладі, doSomething()
виринає у глобальну область видимості, тому вона існує поза блоком if
. ECMAScript 6 стандартизує цю поведінку, щоб прибрати несумісну поведінку браузерів, яка існувала до цього, тож всі оточення ECMAScript 6 повинні працювати однаково.
Можливість використання блочних функцій розширює ваші можливості оголошення функцій у JavaScript, проте ECMAScript 6 також вводить абсолютно новий спосіб оголошення функцій.
Arrow–функції
Однією з найцікавіших частин ECMAScript 6 є arrow–функції. Arrow–функції — це, як можна здогадатись, функції, що оголошуються з новим синтаксисом, який використовує стрілку ("arrow") (=>
). Проте arrow–функції мають кілька важливих відмінностей від JavaScript–функцій:
- Жодних
this
,super
,arguments
таnew.target
зв’язувань – значенняthis
,super
,arguments
таnew.target
всередині функції наслідується від найближчої зовнішньої не–arraw–функції. (super
буде розгянуто у Главі 4.) - Не можуть викликатись з
new
— аrrow–функції не мають методу[[Construct]]
, і тому не можуть використовуватись у якості конструкторів. Arrow–функції кидають помилку при використанні зnew
. - Жодних прототипів — оскільки ви не можете використовувати
new
з arrow–функцією, немає потреби у прототипі. Властивостіprototype
у arrow–функції не існує. - Не можна змінювати
this
— значенняthis
всередині функції не може змінюватись. Воно залишається незмінним протягом усього життєвого циклу функції. - Жодного об’єкту
arguments
— оскільки arrow–функції не мають зв’язування зarguments
, ви мусите покладатись лише на іменовані та залишкові параметри для доступу до аргументів. - Ніяких дубльованих іменованих аргументів — arrow–функції не можуть мати дубьованих іменованих параметрів у строгому та нестрогому режимах, на відміну він не–arrow–функцій, що не можуть мати їх лише у строгому режимі.
Є кілька причин для цих відмінностей. Перша і найголовніша: зв’язування this
є основним джерелом помилок у JavaScript. Дуже легко загубити значення this
всередині функції, що може призвести до неочікуваної поведінки програми. Arrow–функції позбавляють нас цієї проблеми. Друге полягає у тому, що arrow–функцій можуть лише виконувати код з єдиним значенням this
. Рушії JavaScript можуть легше оптимізувати такі операції, на відміну від звичайних функцій, які можуть бути використані в якості конструктора, або бути модифікованим іншим чином.
Решту відмінностей спрямовані на те, щоб скоротити помилки та невизначеності пов’язані з arrow–функціями. Завдяки цьому JavaScript рушії зможуть краще оптимізувати виконання arrow–фукнцій.
I> Зауважте: Arrow–також мають властивість name
, яка формується за тим же правилом, як і в інших функцій.
Синтаксис аrrow–фукнцій
Синтаксис arrow–функцій може бути різним в залежності від того, якого результату ви намагаєтесь досягти. Усі варіанти починаються з аргументів функції, за якими слідує стрілка, за якою слідує тіло функції. Як аргументи, так і тіло можуть набувати різного вигляду, в залежності від використання. До прикладу, наступна arrow–функція приймає один аргумент та просто повертає його:
var reflect = value => value;
// простіший запис для:
var reflect = function(value) {
return value;
};
Коли arrow–функція приймає лише один аргумет, цей один аргумент можна використовувати без будь–якого іншого синтаксису. Далі слідує стрілка та вираз праворуч від стрілки, який буде обчислено та повернено. Не дивлячись на відсутність інструкції return
, ця arrow–функція поверне перший аргумент, який було передано.
Якщо ви передаєте більше, ніж один аргумент, то повинні огорнути їх круглими дужками, ось так:
var sum = (num1, num2) => num1 + num2;
// простіший запис для:
var sum = function(num1, num2) {
return num1 + num2;
};
Функція sum()
просто додає два аргументи та повертає результат. Єдина відмінність між цією arrow–фунцією та функцією reflect()
— аргументи знаходяться в круглих дужках та розділені комами (як і в звичайних функціях).
Якщо функція не приймає аргументів, тоді ви повинні залишити порожні круглі дужки, як показано у прикладі:
var getName = () => "Nicholas";
// простіший запис для:
var getName = function() {
return "Nicholas";
};
Якщо вам потрібне більш традиційне тіло функції, що, можливо, складається з більше, ніж одного виразу, тоді вам потрібно огорнути тіло функції у фігурні дужки та безпосередньо вказати значення, яке має бути повернутим, як у цій версії sum()
:
var sum = (num1, num2) => {
return num1 + num2;
};
// простіший запис для:
var sum = function(num1, num2) {
return num1 + num2;
};
Так чи інакше, ви можете розглядати все, що між фігурними дужками, як звичайну фукнцію, за вийнятком того, що arguments
в ній буде недоступний.
Якщо ви хочете створити функцію, яка не робить нічого, тоді вам потрібно написати порожні фігурні дужки:
var doNothing = () => {};
// простіший запис для:
var doNothing = function() {};
Фігурні дужки використовуються для опису тіла функції і працюють так само, як ви вже бачили в попередніх прикладах. Проте arrow–функція, що повертає літерал об’єкта, має знаходитись у круглих дужках. Наприклад:
var getTempItem = id => ({ id: id, name: "Temp" });
// простіший запис для:
var getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
Знаходження літералу об'єкта у круглих дужках сигналізує, що фігурні дужки є літералом об’єкту, а не тілом функції.
Створення негайно–виконуваних функціональних виразів
Одним з найпопулярніших застосувань функцій в JavaScript є створення негайно–виконуваних функціональних виразів (НВФВ). НВФВ дозволяють вам визначати анонімну функцію та викликати її миттєво, без збереження посилання. Такий підхід стає в нагоді, коли вам потрібно зробити область видимості, захищену від решти програми. Наприклад:
let person = function(name) {
return {
getName: function() {
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"
У цьому коді, НВФВ використовується, щоб створити об’єкт з методом getName()
. Цей метод повертає значення name
, роблячи name
приватним значенням об’єкта, що повертається.
Ви можете зробити те саме, використовуючи arrow–функції, огорнувши її у круглі дужки:
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"
Зауважте, що круглі дужки охоплюють лише вираз arrow–функції, не враховуючи ("Nicholas")
. Це відмінність від звичайних функцій, з якими круглі дужки також можуть огортати параметри, що передаються.
Жодного зв’язування this
Одне з найбільших джерел проблем у JavaScript — це зв’язування this
всередині функцій. Оскільки значення this
всередині функції може змінюватись в залежності від контексту, в якому функція викликається, можливо помилково вплинути на один об’єкт, тоді як ви мали на увазі зовсім інший. Розгляньте такий приклад:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // помилка
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
У цьому коді, об’єкт PageHandler
створений для обробки взаємодій зі сторінкою. Метод init()
викликається для встановлення обробника події і передає керування до this.doSomething()
. Однак цей код не працює так, як очікується.
Виклик this.doSomething()
не спрацює, оскільки this
є посиланням на об’єкт, на який була спрямована подія (у цьому випадку document
), що був пов’язаний з PageHandler
. Якщо ви спробуєте запустити цей код, ви отримаєте помилку, оскільки this.doSomething()
не існує в контексті об’єкту document
.
Ви можете виправити це зв’язуванням значення this
з PageHandler
через використання методу bind()
, ось так:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // немає помилки
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
Тепер цей код працює так, як очікувалось, проте виглядає дещо дивно. Викликаючи bind(this)
, ви насправді створюєте нову функцію, в якої this
є зв’язаним з поточним this
, яким є PageHandler
. Щоб запобігти створенню додаткової фунції, краще скористатись arrow–функцією.
Arrow—функція не має зв’язування з this
. Це означає, що значення this
всередині arrow–функції визначається ланцюжком областей видимості. Якщо arrow–функція міститься всередині не–arrow–функції, this
буде таким, як у зовнішньої функції. В іншому випадку, this
буде не заданим (undefined). Ось один зі способів написати цей код з допомогою arrow–функції:
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
Обробник події у цьому випадку є arrow–функцією, що викликає this.doSomething()
. Значення this
є таким самим, як і всередині init()
, тому такий код працюватиме так само, як і при використанні bind(this)
. Навіть хоча метод doSomething()
не повертає ніякого значення, він є лише однією інструкцією в тілі функції, тому немає потреби огортати його в фігурні дужки.
Arrow–функції є тимчасовими, тому не можуть бути використаними для задання типів — це наслідок відсутності властивості prototype
, яку мають звичайні функції. Якщо ви спробуєте використати оператор new
з arrow–функцією, ви отримаєте помилку, як у цьому прикладі:
var MyType = () => {},
object = new MyType(); // помилка — ви не можете використовувати arrow–функції з 'new'
У цьому коді, виклик new MyType()
падає, оскільки MyType
є arrow–функцією і тому не має внутрішнього методу [[Construct]]
. Знання того, що arrow–функції не можуть використовуватись з new
, дозволяє рушіям JavaScript проводити глибшу оптимізацію їх поведінки.
Також, оскільки значення this
визначається з зовнішньої функції, в якій arrow–функція є визначеною, ви не можете змінити значення this
з допомогою call()
, apply()
або bind()
.
Arrow–функції та масиви
Лаконічний синтакс arrow–функцій робить їх ідеальними для операцій над масивами. До прикладу, якщо ви хочете посортувати масив за власною умовою, ви могли б написати щось таке:
var result = values.sort(function(a, b) {
return a - b;
});
Занадто багато синтаксису для такої простої процедури. Порівняйте це з більш короткою версією, що використовує arrow–функції:
var result = values.sort((a, b) => a - b);
Методи масивів, що приймають функції зворотнього виклику, як от sort()
, map()
та reduce()
, отримають набагато більше користі від використання arrow–функції, які замінюють здавалося би складні процеси простим кодом.
Ніякого зв’язування arguments
Хоча arrow–функції не мають власного об’єкту arguments
, вони можуть звертатись до об’єкта arguments
з батьківської функції. Цей об’єкт arguments
буде доступний незалежно від того, коли arrow–функція буде потім виконана. Наприклад:
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
Всередині createArrowFunctionReturningFirstArg()
створена arrow–функція звертається до елемента arguments[0]
. Це посилання містить перший аргумент, що був переданий у функцію createArrowFunctionReturningFirstArg()
. Якщо arrow–функція виконається пізніше, вона поверне 5
, що і було першим аргументом преданим в createArrowFunctionReturningFirstArg()
. Хоча й arrow–функція більше не в області видимості функції, що створила її, arguments
залишається доступним через ланцюжок контекстів ідентифікатора arguments
.
Ідентифікація аrrow–функцій
Не дивлячись на інший синтакс, arrow–функції є функціями і також можуть бути ідентифікованими. Розгляньте такий код:
var comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true
Вивід console.log()
демонструє, що, як typeof
, так і instanceof
з arrow–функціями поводяться однаково, як і з іншими функціями.
Також, як і з іншими функціями, ви можете використовувати call()
, apply()
та bind()
на arrow–функціями, хоча this
-зв’язування цих функцій не працюватиме. Ось кілька прикладів:
var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3
Функція sum()
викликається з використанням call()
та apply()
для передачі аргументів так, як ви могли б це зробити з будь–якою іншою функцією. Метод bind()
використовується, щоб створити boundSum()
, яка матиме два прив’язаних аргументи 1
та 2
, тож немає необхідності передавати їх безпосередньо.
Arrow–функції доцільно використовувати всюди, де ви зараз використовуєте анонімні функціональні вирази, як ось функції зворотнього виклику. Наступний розділ розгляне інше суттєве покращення ECMAScript 6, яке є внутрішнім і не має нового синтаксису.
Оптимізація хвостового виклику
Напевно, найцікавішою зміною в функціях ECMAScript 6 є оптимізація, яка змінює систему хвостового виклику. Хвостовий виклик (tail call) — це коли функція викликається як остання інструкція в іншій функції, ось так:
function doSomething() {
return doSomethingElse(); // хвостовий виклик
}
Хвостовий виклик імплементований у рушіях ECMAScript 5, обробляється так само, як і виклик будь–якої іншої функції: новий зліпок виклику, що представляє виклик функції, створюється і зберігається в стек викликів. Це означає, що кожен попередній зліпок зберігається в пам’яті, тому це стає проблемою, коли стек викликів стає надто великим.
У чому відмінність?
ECMAScript 6 прагне скоротити розмір стеку для певних хвостових викликів у строгому режимі (у нестрогому режимі хвостові виклики залишаються недоторканими). З такою оптимізацією, замість того щоб створювати новий зліпок для хвостового виклику в стеку, поточний зліпок очищується і перевикористовується доти, доки будуть виконані такі умови:
- Хвостовий виклик не потребує доступу до змінних у поточному зліпку (тобто функція не є замиканням)
- Функція, що робить хвостовий виклик, не робить нічого після повернення результату хвостового виклику
- Результат хвостового виклику повертається як значення функції
Як приклад, такий код легко буде оптимізованийм, оскільки він відповідає всім трьом критеріям:
"use strict";
function doSomething() {
// оптимізовано
return doSomethingElse();
}
Ця функція робить хвостовий виклик doSomethingElse()
, відразу ж повертає результат та не звертається до змінних у локальній області видимості. Одна маленька зміна — не повернення результату — і функція залишеться неоптимізованою:
"use strict";
function doSomething() {
// не оптимізовано — не повертає нічого
doSomethingElse();
}
Так само, якщо ви маєте функцію, що виконує операцію після повернення результату хвостового виклику, тоді функція не буде оптимізованою:
"use strict";
function doSomething() {
// не оптимізовано — додавання після поверненя результату
return 1 + doSomethingElse();
}
Цей приклад додає 1 до результату doSomethingElse()
перед поверненням значення, а цього досить, щоб припинити оптимізацію.
Іншим частим способом ненавмисно вимкнути оптимізацію є збереження результату виклику функції у змінну і потім повернути її значення, як ось тут:
"use strict";
function doSomething() {
// не оптимізовано — виклик не є хвостовим
var result = doSomethingElse();
return result;
}
Цей приклад не може бути оптимізованим, оскільки значення doSomethingElse()
не повертається відразу.
Можливо, найважче уникнути ситуації з використанням замикань. Оскільки замикання має звертатись до змінних із зовнішньої області видимості, хвостова оптимізація може вимкнутись. До прикладу:
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// не оптимізовано — функція є замиканням
return func();
}
У цьому прикладі, замикання func()
звертається до локальної змінної num
. Навіть хоча виклик func()
відразу повертає результат, оптимізація не може бути виконаною через звертання до змінної num
.
Як опанувати оптимізацію хвостового виклику
На практиці, оптимізація хвостового виклику відбувається за кулісами, тож вам не слід думати про це, допоки ви не спробуєте оптимізувати функцію. Основним застосуванням оптимізації хвостових викликів є рекурсивні функції — це той випадок, коли оптимізація матиме значний ефект. Розгляньте цю функцію, яка обчислює факторіал:
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// не оптимізовано — множення після повернення результату
return n * factorial(n - 1);
}
}
Така версія функції не може бути оптимізованою, оскільки після рекурсивного виклику factorial()
відбувається множення. Якщо n
дуже велике число, розмір стек виклику зростатиме і потенційно може спричинити переповнення стеку.
У відповідності до оптимізації функцій, вам потрібно впевнетись, що множення не відбувається після останнього виклику функції. Щоб зробити це, ви можете скористатись параметром за замовчуванням, щоб винести операцію множення з оператора return
. Отримана функція передає тимчасовий результат до наступної ітерації, тому ця функція поводиться так само, але може бути оптимізованою рушієм ECMAScript 6. Ось новий код:
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// оптимізовано
return factorial(n - 1, result);
}
}
У цій переписаній версії factorial()
, другий аргумент p
додається як параметр зі значенням за замовчуванням рівним 1. Параметр p
зберігає результат попереднього множення, тому наступний результат може бути обчислений без виклику функції. Коли n
більше за 1, спершу відбувається множення, і тоді результат передається у якості другого аргументу factorial()
. Це дозволяє рушію ECMAScript 6 оптимізувати рекурсивний виклик.
Оптимізація хвостового виклику це те, про що вам сліду думати тоді, коли ви пишете рекурсивну функцію, тому що це дає суттєвий покращення продуктивності, особливо при застосуванні масивних обчислювальних функцій.
Підсумок
Функції не зазнали величезних змін у ECMAScript 6. Це, скоріше, ряд поступових змін, які полегшують роботу з ними.
Параметри за замовчуванням дозволяють вам легко задавати значення, які будуть використовуватись тоді, коли аргумент не передається. До ECMAScript 6 це потребувало би додаткового надлишкового коду всередині функції для перевірки наявності аргументів та присвоєння значень за замовчуванням.
Залишкові параметри дозволяють вам отримати масив, в якому будуть зберігатись решта переданих параметрів. Використання реального масиву та можливість визначати, які параметри мають бути включені, робить залишкові параметри набагато гнучкішем рішенням, ніж arguments
.
Оператор розкладу є схожим до залишкових параметрів і дозволяє вам розкладати масив на окремі параметри при виклику функції. До ECMAScript 6 було два шляхи передачі окремих параметрів, які зберігаються в масиві: безпосереднє задання кожного параметра або використання apply()
. Використовуючи оператор розкладу, ви можете легко передати масив у будь–яку функцію, не турбуючись про контекст this
цієї функції.
Додаткова властивість name
має допомогти вам легше ідентифікувати функції при пошуку помилок та аналізі. До того ж, ECMAScript 6 формально визначає поведінку блочних функцій, тому вони більше не спричинятимуть помилок у строгому режимі.
У ECMAScript 6 поведінка функції визначається через [[Call]]
при нормальному режимі, або через [[Construct]]
, коли функція визначається з new
. Метавластивість new.target
також дозволяє вам визначати, чи функція була викликана з використанням new
чи ні.
Найбільшою зміною функцій в ECMAScript 6 було введення arrow–функцій. Arrow–функції розроблені для використання замість анонімних функціональних виразів. Arrow–функції мають більш лаконічний синтакс, лексичне this
–зв’язування та не мають об’єкта arguments
. Крім того, arrow–функції не можуть змінювати свій контекст this
та не можуть використовуватись як конструктори.
Оптимізація хвостового виклику дозволяє оптимізувати виклики деяких функцій шляхом збереження малого стеку викликів, ощадливішим використанням пам’яті та попередженням переповнення стеку. Ця оптимізація застосовується рушієм автоматично, коли це безпечно, однак ви можете переписати рекурсивні функції у відповідності до вимог, щоб скористатись перевагами такої оптимізації.