Published on  2021-12-23
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: is not a constructor")

У класах стрілочна-функція має жорстку прив'язку до 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);

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

arrow-function

Висновок

Використання стрілочних-функцій в якості функцій-колбеків: forEach, map, reduce, і т.д - це добрий підхід, намагайтеся його дотримуватися.

Звичайні функції оголошені через ключове слово "function" слід використовувати лише там, де потрібний власний this.

Що стосується класів, зручно додавати обробники подій через стрілочні-функції, через те, що такі функції будуть мати жорстко прив'язаний this і надалі це позбавить від проблем зі зняттям обробників подій.

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