Чем отличаются function declaration, function expression и стрелочная функция?

JuniorJavaScript #javascript #functions

Вопрос

В чём разница между объявлением функции (function declaration), функциональным выражением (function expression) и стрелочной функцией — особенно в контексте hoisting и this?

Короткий ответ

Function declaration полностью поднимается (hoisting) и доступна до строки объявления. Function expression поднимается только как переменная (без значения) или не поднимается вовсе, если объявлена через const/let. Обе имеют собственные this и arguments. Стрелочная функция не поднимается как вызываемая функция, не имеет собственных this, arguments и super — она берёт их из внешней области видимости.

Подробный ответ

Function declaration — function foo() {}. Движок обрабатывает такие объявления на этапе создания области видимости, до выполнения кода построчно, поэтому функцию можно вызвать выше её объявления в коде.

Function expression — функция, присвоенная переменной: const foo = function() {} (может быть анонимной или именованной). Здесь поднимается только переменная (var foo → undefined, let/const foo → temporal dead zone), но не сама функция как значение. Вызвать её до строки присваивания нельзя.

Стрелочная функция — const foo = () => {} — это всегда expression, никогда не declaration. Ключевое отличие не в хостинге, а в поведении:

  • Не имеет собственного this — берёт его лексически, из окружающего кода на момент создания.
  • Не имеет собственного объекта arguments — при обращении берётся arguments из внешней функции (или ошибка, если её нет).
  • Не может быть использована как конструктор (new (() => {})() бросит TypeError).
  • Не имеет prototype.
  • Не может быть привязана через bind/call/apply для изменения this.

Практическое правило: обычные функции (function) использовать для методов объектов и там, где нужен динамический this (например, обработчики событий на DOM-элементах, где this должен быть самим элементом). Стрелочные — для колбэков, где нужно сохранить this внешнего контекста (например, внутри методов класса или React-компонентов).

Подробный разбор поведения this в стрелочных функциях — отдельная тема, здесь важно только то, что стрелочная функция не создаёт свой контекст, а "прозрачно" использует внешний.

Пример

// Hoisting: declaration работает до объявления
console.log(sum(2, 3)) // 5 — работает
function sum(a, b) {
  return a + b
}

// Function expression: TDZ / undefined до присваивания
console.log(typeof multiply) // "undefined" (var) — переменная поднята, значение нет
var multiply = function (a, b) {
  return a * b
}

// console.log(divide()) // ReferenceError: Cannot access 'divide' before initialization
const divide = (a, b) => a / b

// Собственный this у обычной функции vs лексический у стрелочной
const counter = {
  value: 0,
  incRegular: function () {
    setTimeout(function () {
      // this здесь — не counter (в строгом режиме undefined)
      console.log(this === counter) // false
    }, 0)
  },
  incArrow: function () {
    setTimeout(() => {
      // стрелочная берёт this из incArrow, а там this === counter
      console.log(this === counter) // true
    }, 0)
  },
}
counter.incRegular()
counter.incArrow()

// arguments: есть у обычной функции, отсутствует у стрелочной
function regular() {
  return arguments.length
}
console.log(regular(1, 2, 3)) // 3

const arrow = (...args) => args.length // так стрелочная имитирует arguments
console.log(arrow(1, 2, 3)) // 3

Дополнительные вопросы

  • Почему стрелочную функцию нельзя использовать как конструктор с new?
  • Что такое temporal dead zone и как она связана с хостингом let/const?
  • Как метод объекта, объявленный стрелочной функцией, ломает ожидаемое поведение this?