Skip to main content

Відмінності між стрілковою функцією, оголошенням функції та виразом функції

Function declaration, function expression та arrow function - три способи визначити функцію в JavaScript, кожен з яких по-різному поводиться з hoisting (підняттям), прив'язкою this та підтримкою конструктора.

Теорія

TL;DR

  • Function declaration повністю піднімається - можна викликати до рядка де вона написана
  • Function expression і arrow function у TDZ до рядка присвоєння - виклик раніше кидає ReferenceError
  • Arrow function не має власного this - бере його лексично з батьківської області видимості
  • Arrow function не можна використовувати з new - немає внутрішнього методу [[Construct]]
  • Правило вибору: declaration для утиліт верхнього рівня, arrow для колбеків і обробників, expression для IIFEs

Швидкий приклад

javascript
// Declaration: піднімається, працює до оголошення sayHi(); // 'Hi!' - без помилки function sayHi() { console.log('Hi!'); } // Expression: TDZ при const - виклик до рядка кидає помилку // sayHiExpr(); // ReferenceError const sayHiExpr = function() { console.log('Hi from expression!'); }; // Arrow: лексичний this - бере з зовнішньої області const obj = { name: 'World', greet: () => console.log('Hi ' + this.name) // undefined, не 'World' }; obj.greet(); // 'Hi undefined'

Тільки declaration працює до свого рядка. this.name в arrow повертає undefined, бо стрілка захопила this із зовнішньої (модульної) області видимості, а не з obj.

Головна різниця

Найважливіше - це hoisting. Function declaration парситься до виконання будь-якого коду: весь тіл потрапляє у VariableEnvironment виконавчого контексту ще до першого рядка. Expression і arrow піднімають лише змінну: undefined при var, або TDZ при const/let. Плюс до цього, arrow взагалі не створює власних this та arguments - обидва беруться з батьківського лексичного середовища через scope chain.

Коли що використовувати

  • Утиліта верхнього рівня, потрібна з будь-якого місця файлу - function declaration
  • Колбек, метод масиву або обробник події - arrow function (короткий синтаксис, правильний this)
  • IIFE або повернення з функції вищого порядку де важлива назва у stack trace - function expression
  • Конструктор що викликається через new - declaration або expression, але не arrow
  • Обробник події у class-компоненті - arrow (автоматично прив'язує this до instance)

Таблиця порівняння

ХарактеристикаFunction DeclarationFunction ExpressionArrow Function
Синтаксисfunction name() {}const name = function() {}const name = () => {}
HoistingПовний: весь тілТільки змінна (TDZ при const/let)Тільки змінна (TDZ при const/let)
Прив'язка thisДинамічна (залежить від виклику)Динамічна (залежить від виклику)Лексична (зовнішня область видимості)
Об'єкт argumentsТакТакНі - використовуй rest (...args)
Конструктор (new)ТакТакНі - кидає TypeError
super у класахТакТакНі
Коли використовуватиПідняті утиліти, module exportsIIFEs, функції вищого порядкуКолбеки, map/filter, React-обробники

Як це обробляє рушій

V8 парсить function declaration на пре-виконавчій фазі: повний об'єкт функції потрапляє у VariableEnvironment виконавчого контексту ще до першого рядка коду. Expression і arrow парсяться як літерали під час виконання - прив'язуються до змінної лише коли інтерпретатор доходить до цього рядка. Arrow взагалі не створює прив'язок для this та arguments: при зверненні до this всередині стрілки рушій іде вгору по scope chain до батьківського LexicalEnvironment.

Типові помилки

Виклик arrow до її оголошення:

javascript
greet(); // ReferenceError: Cannot access 'greet' before initialization const greet = () => console.log('hi');

const тримає greet у TDZ. Перенеси виклик нижче або використай function declaration.

Очікуваний динамічний this у методі-стрілці:

javascript
const obj = { name: 'Alice', say: () => console.log(this.name) // undefined, не 'Alice' }; obj.say();

Arrow взяла this з модульної області, а не з obj. Використовуй звичайний метод: say() { console.log(this.name); }.

arguments у стрілковій функції:

javascript
const sum = () => console.log(arguments); // ReferenceError або зовнішній arguments sum(1, 2, 3);

Arrow не має об'єкту arguments. Замість нього - rest-параметри: const sum = (...args) => args.reduce((a, b) => a + b, 0).

Arrow як конструктор:

javascript
const User = () => ({ name: 'Bob' }); new User(); // TypeError: User is not a constructor

Немає [[Construct]] - немає new. Використовуй function User() {} або class User {}. Ця помилка трапляється в code review частіше ніж здається - зазвичай коли хтось переписує метод класу в стрілку і забуває оновити місця виклику.

super у arrow-полі класу:

javascript
class Child extends Parent { // arrow = () => super.method(); // SyntaxError method() { super.method(); } // правильно }

Де зустрічається в реальних проєктах

  • React - arrow class fields (handleSubmit = () => {}) щоб прив'язати this до instance без .bind(this) у конструкторі
  • Express.js - arrow у route-колбеках (app.use((req, res, next) => {})) для передбачуваного this
  • Redux - arrow-селектори (const getUsers = state => state.users)
  • Node.js streams - function declaration для парсерів що викликаються з кількох місць у коді
  • Lodash - named function expression у _.curry(function add(a, b) {}) щоб рекурсивні виклики і stack traces залишалися читабельними

Питання на співбесіді

Q: Що відбудеться якщо оголосити function expression через var замість const?
A: Змінна підійметься як undefined. Виклик до рядка присвоєння дасть TypeError: sayHi is not a function - не ReferenceError, бо змінна існує, але містить undefined.

Q: Чому у arrow function немає prototype?
A: Специфікація не додає prototype до arrow, бо їх не можна використовувати з [[Construct]]. Немає сенсу виділяти властивість яка ніколи не буде використана.

Q: Чи змінюється this у function declaration у strict mode?
A: Так. У strict mode this всередині функції без явного контексту виклику стає undefined, а не глобальний об'єкт. Arrow це не стосується, бо вона не створює власного this.

Q: Чи можна дати arrow function ім'я для кращих stack traces?
A: Коли присвоюєш стрілку іменованій const, рушій виводить ім'я змінної як ім'я функції - воно видно у stack trace. Але написати іменовану стрілку напряму так само як function log() {} не вийде.

Q: (Senior) Яка різниця між IIFE-стрілкою з const і function expression з var з точки зору TDZ?
A: const f = (() => {})() виконується нормально на цьому рядку - TDZ блокує лише звернення до f до нього. З var f = (function(){})() - var підіймається як undefined, тому звернення до f до рядка дає undefined, а виклик f() кидає TypeError: f is not a function. Різні помилки з різними повідомленнями - і це важливо при дебагу.

Приклади

Hoisting на практиці

javascript
// Declaration доступна відразу console.log(add(2, 3)); // 5 function add(a, b) { return a + b; } // Expression з const: TDZ до рядка присвоєння // console.log(multiply(2, 3)); // ReferenceError const multiply = function(a, b) { return a * b; }; console.log(multiply(2, 3)); // 6

Виклик add на рядку 2 працює бо declaration була піднята разом із тілом. Закоментований виклик multiply кинув би ReferenceError - const тримає змінну в TDZ до рядка присвоєння.

Arrow function і this у React class-компоненті

javascript
class Button extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; // Expression: потребує .bind(this) щоб отримати правильний контекст this.handleClickExpr = function() { this.setState({ count: this.state.count + 1 }); }.bind(this); } // Arrow: захоплює this з тіла класу, bind не потрібен handleClickArrow = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <button onClick={this.handleClickArrow}>{this.state.count}</button>; } }

Expression-варіант потребує .bind(this), інакше при виклику з браузера this буде undefined. Arrow захоплює this з тіла класу автоматично. Саме тому arrow class fields стали стандартним патерном у pre-hooks React.

Arrow як конструктор - де падає у продакшені

javascript
function Decl() { this.type = 'declaration'; } const Expr = function() { this.type = 'expression'; }; const Arrow = () => {}; new Decl(); // { type: 'declaration' } new Expr(); // { type: 'expression' } new Arrow(); // TypeError: Arrow is not a constructor // Реальний патерн що ламається у рантаймі function createInstance(Ctor) { return new Ctor(); } createInstance(Decl); // ок createInstance(Arrow); // TypeError у продакшені

new Arrow() падає бо arrow не має методу [[Construct]]. Небезпека в тому що помилки компіляції немає. Якщо передати arrow у фабрику що очікує конструктор - креш вилізе в рантаймі, не під час розробки.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?