Read 11 min
Arrow functions
Функції-стрілки з'явилися у стандарті ES6 (ECMAScript 2015) та їх поява дозволила писати JS розробникам більш компактний та локонічний код.
Приклад без використання функції-стрілки:
const result = [1, 2, 3].map(function (item) {
return item * 2;
});З використанням функції-стрілки приклад вище може виглядати так:
const result = [1, 2, 3].map(item => item * 2);Код став компактнішим за рахунок того, що ми позбулись:
- Кругих дужок навколо єдиного аргументу функції (при використанні більше одного аргументу, дужки обов'язкові)
- фігурних дужок навколо тіла функції
- ключового слова
function - ключового слова
return
Ці синтаксичні особливості також дозволяють використовувати такий короткий запис:
const sum = a => b => c => a + b + c;Код, наведений вище, еквівалентний запису з використанням ключового слова function:
function sum (a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}Погодьтеся, ми заощадили порядну кількість рядків коду.
Окрім компактності та лаконічності, які дає використання стрілочної-функції, вона має ряд наступних відмінностей
від функції оголошеної через ключове слово function
"Arrow function" не має свого власного this
const someArrowFunction = () => {
console.log(this);
}
someArrowFunction() // windowУ прикладі вище ми не використовуємо директиву use strict, тому виклик функції-стрілки у глобальній області дає
очікуваний результат - значення window (домовимось, що ми запускаємо код у браузері, для іншого середовища виконання
це значення може бути іншим).
А якщо стрілочна-функція буде використовуватися як метод об'єкта? Давайте порівняємо її поведінку зі звичайною функцією:
const user = {
firstName: 'John',
getNameArrow: () => {
return this.firstName;
},
getName() {
return this.firstName;
}
};
console.log(user.getNameArrow()); // undefined
console.log(user.getName()); // "John"Виклик методу getName, як і передбачалося, повернув нам очікуваний результат - "John", значення якого міститься
як об'єкт firstName.
А ось виклик методу getNameArrow повернув undefined - не зовсім те, що ми би хотіли отримати.
Хоча, звичайно, така поведінка є абсолютно коректною.
Вся справа саме в тому, що у стрілочної-функції відсутній власний this,
а використовується значення this навколишнього контексту, або іншими словами батьківського
лексичного оточення (LexicalEnvironment)
Чим же така особливість може бути корисною? Давайте трохи модифікуємо наш приклад:
- додамо в
userмасив зі списком друзів та метод який дозволить сказати "hi" кожному другу; - відмовимося від стрілочної-функції і використаємо звичайну функцію в методі
forEach
const user = {
firstName: 'John',
friends: ['Peter', 'Kevin', 'Nick'],
seyHi() {
this.friends.forEach(function (friend) {
console.log(this.firstName + ' says hi 👋 to ', friend);
});
}
};
user.seyHi();
// undefined says hi 👋 to Peter
// undefined says hi 👋 to Kevin
// undefined says hi 👋 to NickЗнову не зовсім те, чого ми хотіли отримати, правда?
У undefined не може бути друзів! А якщо без жартів, що конкретно пішло не за планом?
Адже ми відмовилися від використання стрілочної-функції.
Саме з цією особливістю поведінки "звичайних" функцій пов'язана ще одна причина появи стрілочних-функцій, окрім бажання JavaScript розробників писати менше коду, економлячи на слові "function" 😀
Справа в тому, що в JavaScript кожна зі "звичайних" функцій має свій контекст виконання,
або свій this простіше кажучи. У цьому прикладі this для функції всередині методу forEach
буде посилатися на глобальний об'єкт.
Чому глобальний об'єкт? Справа в тому, що функція всередині методу forEach викликається окремо від об'єкта,
по суті на кожну ітерацію буде виконано наступний виклик:
(function (friend) {
console.log(this.firstName + ' says hi 👋 to ', friend);
})()JavaScript динамічно вираховує значення this - не знаходить у рамках або контексті якого об'єкта повинна
бути виконана функція і підставляє window як значення this у даній функції.
Раніше для вирішення цієї проблеми використовували кілька підходів:
- зберігали значення
thisбатьківського оточення в якусь змінну з назвоюselfабоthat
const user = {
firstName: 'John',
friends: ['Peter', 'Kevin', 'Nick'],
seyHi() {
const self = this;
this.friends.forEach(function (friend) {
console.log(self.firstName + ' says hi 👋 to ', friend);
});
}
};- якщо сигнатура методу дозволяла, а
forEachдозволяє, передавали, як другий аргумент, значенняthisдля колбека
const user = {
firstName: 'John',
friends: ['Peter', 'Kevin', 'Nick'],
seyHi() {
this.friends.forEach(function (friend) {
console.log(this.firstName + ' says hi 👋 to ', friend);
}, this); }
};Думаю тепер стало ясно, чому такі методи масивів, наприклад:
forEach,
filter,
some,
every
історично мають необов'язковий thisArg аргумент.
Стрілочна-функція не має свого власного псевдомасиву arguments
Логіка абсолютно така ж як і з використанням this - найчастіше у функціях-колбеках були необхідні
саме аргументи з батьківської функції.
Але з приходом "спред синтаксису" (spread syntax)
використання псевдомасив arguments пропало як явище.
При необхідності отримати всі аргументи функції або методу застосовують наступний підхід:
function someFunction (...props) {
console.log(props, Array.isArray(props)); // true - 👍
console.log(arguments, Array.isArray(arguments)); // false - 👎
}До стрілочної-функції неможливо "прибіндити" контекст
Метод bind, власного кажучи як call та apply, не працює у зв'язці зі стрілочною-функцією,
причина наступна – відсутність власного контексту (this) який може бути замінений
при використанні методу bind.
const obj = {
firstName: 'John'
};
const arrow = () => console.error(this.firstName);
console.log(arrow.call(obj)); // undefined
console.log(arrow.apply(obj)); // undefined
console.log(arrow.bind(obj)()); // undefined Стрілочну-функцію неможливо викликати з ключовим словом new
До появи в JavaScript класів та функцій стрілок, створення однотипних об'єктів здійснювалося з
використанням функцій-конструкторів. Функцією конструктором могла бути будь-яка функція, викликана з
використанням ключового слова new.
Наприклад ось такий виклик, поверне порожній об'єкт:
function SomeFunction () {}
const obj = new SomeFunction(); // {}Справа в тому, що даний підхід - використання ключового слова new, мав на меті спростити життя розробникам,
і робив деякі речі "під капотом", а саме привласнював у this
порожній об'єкт і автоматично повертав його з функції
function SomeFunction () {
// this = {};
// return this;
}І як можно здогадатися, стрілочна-функція не може бути викликана з ключовим словом new все з тієї ж причини,
за якою її не можливо "прибіндити", або іншими словами, використовувати
у зв'язці з методами call, apply, bind
(отримаємо помилку: "Uncaught TypeError:
У класах стрілочна-функція має жорстку прив'язку до this
Давайте спочатку розглянемо приклад з об'єктом, в якому ми втратимо контекст:
const user = {
firstName: 'John',
getFirstName () {
return this.firstName;
}
};
const getName = user.getFirstName;
console.log(getName()); // undefinedМи зберегли посилання на метод об'єкта getFirstName у змінну getName у рядку 8 та спробували його викликати.
В результаті отримали undefined, тому що JavaScript не зміг визначити в контексті якого об'єкта ми викликали
функцію getName, точніше так: JavaScript для цієї функції визначив контекст як глобальний об'єкт window.
Давайте трохи модифікуємо наш приклад. Тепер метод getFirstName повертатиме стрілочну-функцію,
яка у свою чергу повертатиме значення firstName об'єкту user.
const user = {
firstName: 'John',
getFirstName () {
return () => this.firstName;
}
};
const getName = user.getFirstName();
console.log(getName()); // JohnВ даному випадку все відпрацювало коректно, контекст не було втрачено.
Тепер давайте подивимося на поведінку стрілочної-функції у рамках класу:
class User {
constructor (name) {
this.name = name;
this.getName = () => this.name;
}
}
const user = new User('John');
const getName = user.getName;
console.log(getName()); // JohnЯк видно з прикладу вище, контекст не було втрачено. Стрілочна-функція надійно прив'язана до об'єкту який було сконструйовано за допомогою класу.
Даний прийом зручно використовувати при додаванні/видаленні обробників подій усередині класу:
class User {
onHandleClick = () => {
console.log(this.firstName);
};
constructor(firstName) {
this.firstName = firstName;
this.render();
this.initEventListeners();
}
initEventListeners() {
this.element.addEventListener('click', this.onHandleClick);
}
removeEventListeners() {
this.element.removeEventListener('click', this.onHandleClick);
}
render() {
const element = document.createElement('div');
element.innerHTML = `<button>Click me maybe!</button>`;
this.element = element;
}
destroy() {
this.element.remove();
this.removeEventListeners();
}
}У класах стрілочна-функція завжди прив'язана до об'єкта, а не до його прототипу
При створенні об'єкта за допомогою класу, стрілочна-функція потрапить на сам об'єкт, а не на його прототип.
Давайте подивимося на приклад.
class User {
getNameArrow = () => {
return this.firstName;
}
constructor (firstName) {
this.firstName = firstName;
}
getName() {
return this.firstName;
}
}
const user = new User('John');
console.log(user);При виведенні об'єкта в консоль, можна побачити, що стрілочна-функція належить до властивостей створеного об'єкта.
Висновок
Використання стрілочних-функцій в якості функцій-колбеків: forEach, map, reduce,
і т.д - це добрий підхід, намагайтеся його дотримуватися.
Звичайні функції оголошені через ключове слово "function" слід використовувати лише там,
де потрібний власний this.
Що стосується класів, зручно додавати обробники подій через стрілочні-функції,
через те, що такі функції будуть мати жорстко прив'язаний this і надалі це позбавить від проблем
зі зняттям обробників подій.
Увага! Не зловживайте стрілочними функціями у класах, оскільки вони стають властивостями самого об'єкта, а не його прототипу, це може вплинути на продуктивність під час використання такого підходу у великих масштабах