Відмінності між стрілковою функцією, оголошенням функції та виразом функції
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
Швидкий приклад
// 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 Declaration | Function Expression | Arrow Function |
|---|---|---|---|
| Синтаксис | function name() {} | const name = function() {} | const name = () => {} |
| Hoisting | Повний: весь тіл | Тільки змінна (TDZ при const/let) | Тільки змінна (TDZ при const/let) |
Прив'язка this | Динамічна (залежить від виклику) | Динамічна (залежить від виклику) | Лексична (зовнішня область видимості) |
Об'єкт arguments | Так | Так | Ні - використовуй rest (...args) |
Конструктор (new) | Так | Так | Ні - кидає TypeError |
super у класах | Так | Так | Ні |
| Коли використовувати | Підняті утиліти, module exports | IIFEs, функції вищого порядку | Колбеки, map/filter, React-обробники |
Як це обробляє рушій
V8 парсить function declaration на пре-виконавчій фазі: повний об'єкт функції потрапляє у VariableEnvironment виконавчого контексту ще до першого рядка коду. Expression і arrow парсяться як літерали під час виконання - прив'язуються до змінної лише коли інтерпретатор доходить до цього рядка. Arrow взагалі не створює прив'язок для this та arguments: при зверненні до this всередині стрілки рушій іде вгору по scope chain до батьківського LexicalEnvironment.
Типові помилки
Виклик arrow до її оголошення:
greet(); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => console.log('hi');const тримає greet у TDZ. Перенеси виклик нижче або використай function declaration.
Очікуваний динамічний this у методі-стрілці:
const obj = {
name: 'Alice',
say: () => console.log(this.name) // undefined, не 'Alice'
};
obj.say();Arrow взяла this з модульної області, а не з obj. Використовуй звичайний метод: say() { console.log(this.name); }.
arguments у стрілковій функції:
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 як конструктор:
const User = () => ({ name: 'Bob' });
new User(); // TypeError: User is not a constructorНемає [[Construct]] - немає new. Використовуй function User() {} або class User {}. Ця помилка трапляється в code review частіше ніж здається - зазвичай коли хтось переписує метод класу в стрілку і забуває оновити місця виклику.
super у arrow-полі класу:
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 на практиці
// 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-компоненті
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 як конструктор - де падає у продакшені
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 у фабрику що очікує конструктор - креш вилізе в рантаймі, не під час розробки.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.