5 відмінностей між звичайною та стрілковою функцією

8 хв. читання

У JavaScript ми можемо оголосити функцію кількома способами:

  • За допомогою ключового слова function:
// декларація функції
function greet(who) {
  return `Hello, ${who}!`;
}
// функціональний вираз
const greet = function(who) {
  return `Hello, ${who}`;
}

У статті ми вважатимемо звичайною функцією і функціональний вираз, і декларацію функції.

  • Починаючи з ES2015, розробникам став доступним також синтаксис стрілкових функцій:
const greet = (who) => {
  return `Hello, ${who}!`;
}

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

1. this

1.1 Звичайна функція

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

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

function myFunction() {
  console.log(this);
}

// простий виклик 
myFunction(); // виведе глобальний об'єкт (window)

Під час виклику функції як метода this відповідатиме об'єкту, для якого викликано цей метод.

const myObject = {
  method() {
    console.log(this);
  }
};
// виклик метода
myObject.method(); // виведе myObject

Під час непрямого виклику за допомогою myFunc.call(thisVal, arg1, ..., argN) або myFunc.apply(thisVal, [arg1, ..., argN]) значення this прирівнюється до першого аргументу:

function myFunction() {
  console.log(this);
}

const myContext = { value: 'A' };

myFunction.call(myContext); // виводить { value: 'A' }
myFunction.apply(myContext); // виводить { value: 'A' }

Якщо ви викликаєте конструктор за допомогою new, то this буде прив'язаний до щойно створеного екземпляра:

function MyFunction() {
  console.log(this);
}

new MyFunction(); // виводить екземпляр MyFunction

1.2 Стрілкова функція

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

У цьому прикладі myMethod — зовнішня функція для стрілкової функції callback():

const myObject = {
  myMethod(items) {
    console.log(this); // виводить myObject
    const callback = () => {
      console.log(this); // виводить myObject
    };
    items.forEach(callback);
  }
};

myObject.myMethod([1, 2, 3]);

Значення this всередині стрілкової функції callback() тут дорівнюватиме this зовнішньої функції myMethod().

Можливість визначити this лексично — одна з найбільших переваг стрілкових функцій. При використанні колбеків в методах ви можете бути впевнені, що стрілкова функція не визначає власного this (прощавайте, const self = this або callback.bind(this)).

На відміну від звичайної функції, непрямий виклик стрілкової функції за допомогою myArrowFunc.call(thisVal) або myArrowFunc.apply(thisVal) не приведе до зміни this: контекст завжди визначатиметься з лексичного оточення.

2. Конструктори

2.1 Звичайна функція

Як ми вже з'ясували, зі звичайної функції виходять чудові конструктори. Наприклад, Car() створює екземпляр об'єкта Car:

function Car(color) {
  this.color = color;
}

const redCar = new Car('red');
redCar instanceof Car; // => true

Car() — звичайна функція, але у поєднанні з new вона перетворюється на конструктор і створює екземпляр класу.

2.2 Стрілкова функція

Лексичне визначення this стає недоліком для стрілкової функції, коли справа доходить до конструктора.

Якщо ви спробуєте виконати стрілкову функцію з ключовим словом new, JavaScript викличе помилку:

const Car = (color) => {
  this.color = color;
};

const redCar = new Car('red'); // TypeError: Car is not a constructor

Як бачимо, викликавши new Car('red') зі стрілковою функцією Car, отримуємо помилку: TypeError: Car is not a constructor.

3. Об'єкт arguments

3.1 Звичайна функція

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

Викличемо функцію myFunction з двома аргументами:

function myFunction() {
  console.log(arguments);
}

myFunction('a', 'b'); // logs { 0: 'a', 1: 'b'}

Як бачимо, об'єкт arguments зберіг параметри a та b, з якими викликалась функція.

3.2 Стрілкова функція

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

Перевіримо на практиці:

function myRegularFunction() {
  const myArrowFunction = () => {
    console.log(arguments);
  }

  myArrowFunction('c', 'd');
}

myRegularFunction('a', 'b'); // logs { 0: 'a', 1: 'b' }

Стрілкова функція myArrowFunction() викликається з аргументами c та d. Всередині стрілкової функції ми виводимо об'єкт arguments і бачимо, що він зберігає параметри, з якими була викликана зовнішня функція myRegularFunction().

Якщо вам потрібні параметри саме стрілкової функції, на допомогу приходить синтаксис ...rest-параметрів.

function myRegularFunction() {
  const myArrowFunction = (...args) => {
    console.log(args);
  }

  myArrowFunction('c', 'd');
}

myRegularFunction('a', 'b'); // logs { 0: 'c', 1: 'd' }

Тут ...args збирає параметри, з якими викликалась безпосередньо стрілкова функція: { 0: 'c', 1: 'd' }.

4. Неявний return

4.1 Звичайна функція

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

function myFunction() {
  return 42;
}

myFunction(); // => 42

Якщо функція не викликає return, або після його виклику немає виразу, то така функція неявно повертає undefined:

function myEmptyFunction() {
  42;
}

function myEmptyFunction2() {
  42;
  return;
}

myEmptyFunction();  // => undefined
myEmptyFunction2(); // => undefined

4.2 Стрілкова функція

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

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

const increment = (num) => num + 1;

increment(41); // => 42

Стрілкова функція increment() неявно повертає результат операції num+1 без ключового слова return.

5. Методи

5.1 Звичайна функція

Найпоширеніший спосіб оголосити методи в класах — використати звичайні функції. В цьому класі Hero метод logName() оголошено за допомогою звичайної функції.

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }

  logName() {
    console.log(this.heroName);
  }
}

const batman = new Hero('Batman');

І зазвичай це слушний вибір. Однак іноді вам треба передати метод як колбек (наприклад в setTimout() чи до слухача подій), і тоді this може призвести до неочікуваних помилок.

Для прикладу візьмемо метод logName(), який передається як колбек до setTimeout():

setTimeout(batman.logName, 1000);
// через 1 секунду виведе "undefined"

Через 1 секунду в консоль буде виведено undefined. Усе тому, що setTimout() виконує logName() у звичайний спосіб (тобто this буде глобальним об'єктом). Так трапляється, коли метод відділено від об'єкта.

Прив'яжемо this до правильного контексту вручну:

setTimeout(batman.logName.bind(batman), 1000);
// через 1 секунду виведе "Batman"

batman.logName.bind(batman) прив'язує this до екземпляра batman. Тепер можна бути впевненими, що метод не втратить свій контекст при виконанні.

Якщо вручну прив'язувати this, з'являється надлишковий код, особливо якщо у вас багато методів. Існує спосіб цього уникнути: стрілкова функція у якості поля класу.

5.2 Стрілкова функція

Завдяки пропозиції полів класу (на момент написання статті вона на третій стадії) ми можемо використовувати стрілкові функції як методи класів.

На відміну від звичайних функцій, у стрілкових функціях this прив'язується лексично до екземпляра класу.

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

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }

  logName = () => {
    console.log(this.heroName);
  }
}

const batman = new Hero('Batman');

Тепер ми можемо використати batman.logName як колбек і не турбуватись про прив'язку this. У методі logName() this буде завжди прив'язаний до екземпляра класу:

setTimeout(batman.logName, 1000);
// через 1 секунду виведе "Batman"

Підсумуємо

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

Якщо коротко:

  • Значення this всередині звичайної функції динамічне, тому залежить від способу виклику функції, а всередині стрілкової функції визначається лексично, тобто дорівнює this зовнішньої функції.
  • Об'єкт arguments всередині звичайної функції містить список аргументів, з якими викликалась функція. Стрілкова функція сама по собі не підтримує arguments. Аби отримати перелік її аргументів, використайте rest-параметр ...args.
  • Якщо в стрілковій функції один вираз, то його результат повертається неявно, навіть без ключового слова return.
  • Ви можете визначати методи класів за допомогою стрілкових функцій — і не турбуватись про прив'язку this до екземпляра класу. Особливо це корисно для колбеків, адже this все одно залишатиметься прив'язаним до екземпляра класу.
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.3K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація