У 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
все одно залишатиметься прив'язаним до екземпляра класу.
Ще немає коментарів