Learning Book

Частые проблемы

Цикл с var и setTimeout

Самый известный баг замыканий:

// ❌ Выведет "5 5 5 5 5"
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100)
}

var создаёт одну переменную i на весь вызов. Все 5 колбэков замыкаются на неё. К моменту срабатывания таймеров цикл завершён, i === 5.

Решение: let

// ✅ Выведет "0 1 2 3 4"
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100)
}

let создаёт новую привязку на каждой итерации. Каждый колбэк замыкается на свою копию i.

Решение: IIFE (до ES2015)

for (var i = 0; i < 5; i++) {
  ;(function (j) {
    setTimeout(() => console.log(j), 100)
  })(i)
}

IIFE создаёт новое окружение с параметром j, копируя текущее значение i.

Утечки памяти

Замыкание удерживает окружение внешней функции. Если в нём тяжёлые объекты — они не будут собраны GC:

// ❌ heavyData висит в памяти, пока жив обработчик
function setup() {
  const heavyData = new Array(1_000_000).fill('x')

  document.getElementById('btn').addEventListener('click', () => {
    console.log('Кнопка нажата')
    // heavyData не используется, но замыкание удерживает окружение
  })
}

Как избежать

Извлечь нужное до создания замыкания:

// ✅ heavyData может быть собран GC
function setup() {
  const heavyData = loadData()
  const summary = heavyData.length

  document.getElementById('btn').addEventListener('click', () => {
    console.log(`Элементов: ${summary}`)
  })
}

Всегда снимай обработчики, когда они больше не нужны:

function attachHandler(element) {
  const handler = () => { /* ... */ }
  element.addEventListener('click', handler)
  return () => element.removeEventListener('click', handler)
}

this и замыкания

Замыкание не захватывает this — значение this определяется способом вызова:

const obj = {
  name: 'Объект',
  getName() {
    return function () {
      return this.name // ❌ this зависит от способа вызова
    }
  },
}

const fn = obj.getName()
fn() // undefined (this !== obj)

Стрелочные функции — исключение. Они берут this из лексического окружения:

const obj = {
  name: 'Объект',
  getName() {
    return () => this.name // ✅ this из getName
  },
}

obj.getName()() // "Объект"

Stale closure в React

Каждый рендер создаёт новое замыкание. Проблема — когда старое замыкание использует устаревшие данные:

// ❌ count всегда 0 в замыкании setInterval
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1) // замыкание захватило count = 0
    }, 1000)
    return () => clearInterval(timer)
  }, []) // пустой deps → замыкание создано один раз
}

Замыкание setInterval захватило count = 0 первого рендера. На каждом тике: 0 + 1 = 1.

// ✅ Функциональный updater — не зависит от замыкания
setCount(prev => prev + 1)

Итого

ПроблемаПризнакРешение
Цикл с varВсе коллбэки видят одно значениеlet или IIFE
Утечка памятиОбработчик держит тяжёлые данныеИзвлечь нужное, removeEventListener
this в замыканииthis === undefinedСтрелочная функция
Stale closureДанные устарели (React, setInterval)Функциональный updater, актуальный deps