← Курс/Что такое замыкание? Покажи на примере.#127 из 257+40 XP

Что такое замыкание? Покажи на примере.

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

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

Полный разбор

Лексическое окружение

Каждая функция при создании сохраняет ссылку на **лексическое окружение** — объект со всеми переменными, доступными в том месте, где функция была написана в коде.

function outer() {
  const secret = 42  // переменная внешней функции

  function inner() {
    // inner "видит" secret, потому что была создана внутри outer
    console.log(secret)  // 42
  }

  return inner
}

const fn = outer()  // outer выполнилась и "закончилась"
fn()                // но inner всё ещё помнит secret!

inner — это замыкание. Она захватила переменную secret из лексического окружения outer.

Пример 1: Счётчик (приватное состояние)

function createCounter(initialValue = 0) {
  let count = initialValue  // приватная переменная

  return {
    increment() { return ++count },
    decrement() { return --count },
    reset()     { count = initialValue; return count },
    getValue()  { return count },
  }
}

const counter = createCounter(10)
console.log(counter.getValue())   // 10
console.log(counter.increment())  // 11
console.log(counter.increment())  // 12
console.log(counter.decrement())  // 11
console.log(counter.reset())      // 10

// count недоступна снаружи — это и есть инкапсуляция!
console.log(counter.count)  // undefined

Пример 2: Фабричная функция

function makeMultiplier(factor) {
  // factor "запоминается" каждым созданным умножителем
  return (number) => number * factor
}

const double  = makeMultiplier(2)
const triple  = makeMultiplier(3)
const times10 = makeMultiplier(10)

console.log(double(5))   // 10
console.log(triple(5))   // 15
console.log(times10(5))  // 50

// Каждая функция — отдельное замыкание со своим factor

Пример 3: Мемоизация

function memoize(fn) {
  const cache = {}  // кеш "живёт" в замыкании

  return function(...args) {
    const key = JSON.stringify(args)

    if (key in cache) {
      console.log('Из кеша:', key)
      return cache[key]
    }

    const result = fn(...args)
    cache[key] = result
    return result
  }
}

const expensiveCalc = memoize((n) => {
  // имитация долгого вычисления
  return n * n
})

console.log(expensiveCalc(10))  // вычисляет: 100
console.log(expensiveCalc(10))  // из кеша: 100
console.log(expensiveCalc(20))  // вычисляет: 400

Классический баг: var в цикле

Это **самый популярный вопрос** на собеседованиях про замыкания:

// БАग: var имеет function scope, все колбэки замкнуты на ОДНУ переменную i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Выводит: 3, 3, 3  (НЕ 0, 1, 2!)
// Почему? К моменту выполнения setTimeout, цикл уже завершился и i = 3

// ИСПРАВЛЕНИЕ 1: let (block scope — каждая итерация имеет свой i)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Выводит: 0, 1, 2

// ИСПРАВЛЕНИЕ 2: IIFE (немедленно вызываемая функция создаёт новую область)
for (var i = 0; i < 3; i++) {
  ;(function(j) {
    setTimeout(() => console.log(j), 100)
  })(i)
}
// Выводит: 0, 1, 2

Паттерн Module (замыкание как модуль)

const shoppingCart = (function() {
  // Приватные данные — недоступны снаружи
  const items = []
  let discount = 0

  // Публичный API
  return {
    addItem(item)    { items.push(item) },
    removeItem(name) {
      const idx = items.findIndex(i => i.name === name)
      if (idx !== -1) items.splice(idx, 1)
    },
    setDiscount(pct) { discount = pct },
    getTotal() {
      const subtotal = items.reduce((sum, item) => sum + item.price, 0)
      return subtotal * (1 - discount / 100)
    },
    getItems() { return [...items] },  // возвращаем копию, не оригинал
  }
})()

shoppingCart.addItem({ name: 'Книга', price: 500 })
shoppingCart.addItem({ name: 'Курс', price: 2000 })
shoppingCart.setDiscount(10)
console.log(shoppingCart.getTotal())  // 2250
console.log(shoppingCart.items)       // undefined — приватно!

Замыкания и утечки памяти

Замыкания хранят ссылки на внешние переменные → переменные не удаляются сборщиком мусора. Будь осторожен с замыканиями в обработчиках событий — всегда удаляй их при уничтожении компонента.

Связанные уроки курса

  • Замыкания — базовый разбор
  • Переменные (var/let/const и область видимости)
  • var и hoisting
  • Как отвечать на собеседовании

    **Начни с определения**: "Замыкание — это функция, которая помнит своё лексическое окружение. Даже после завершения внешней функции, внутренняя сохраняет доступ к переменным."

    **Сразу покажи пример**: createCounter() — самый чистый и понятный пример. Показывает приватное состояние и практическую ценность.

    **Упомяни классический баг**: var в цикле с setTimeout. Это обязательная часть ответа — любой опытный интервьюер спросит об этом.

    **Практическая ценность**: упомяни мемоизацию, паттерн модуль, фабричные функции.

    Красные флаги ответа

    1. **"Замыкание — это когда функция внутри функции"** — это описание синтаксиса, не определение. Главное — захват переменных из внешнего окружения.

    2. **Не знать классический баг с var в цикле** — это проверяется на 90% собеседований. Незнание выдаёт поверхностное понимание.

    3. **Не понимать практических применений** — если ты знаешь что такое замыкание, но не можешь назвать зачем оно нужно — это слабый ответ.

    Примеры

    createCounter, makeMultiplier и memoize — три классических примера замыканий на собеседовании

    // ===== 1. СЧЁТЧИК: приватное состояние =====
    function createCounter(start = 0, step = 1) {
      let count = start  // приватная переменная
    
      return {
        next()    { count += step; return count },
        prev()    { count -= step; return count },
        reset()   { count = start; return count },
        current() { return count },
        // Метод для создания нового счётчика с текущим значением
        clone()   { return createCounter(count, step) }
      }
    }
    
    const counter = createCounter(0, 2)  // начинаем с 0, шаг 2
    console.log(counter.next())    // 2
    console.log(counter.next())    // 4
    console.log(counter.next())    // 6
    console.log(counter.current()) // 6
    console.log(counter.reset())   // 0
    
    const clone = counter.clone()  // клон с текущим состоянием (0)
    console.log(clone.next())      // 2 (независимый счётчик)
    console.log(counter.next())    // 2 (оригинал тоже на 2)
    
    // ===== 2. ФАБРИЧНАЯ ФУНКЦИЯ =====
    function makeMultiplier(factor) {
      return (n) => n * factor  // захватывает factor
    }
    
    const double  = makeMultiplier(2)
    const triple  = makeMultiplier(3)
    
    // Каждая функция — отдельное замыкание
    console.log('\ndouble(7):', double(7))   // 14
    console.log('triple(7):', triple(7))   // 21
    
    // Применение к массиву
    const nums = [1, 2, 3, 4, 5]
    console.log('doubled:', nums.map(double))  // [2, 4, 6, 8, 10]
    console.log('tripled:', nums.map(triple))  // [3, 6, 9, 12, 15]
    
    // ===== 3. МЕМОИЗАЦИЯ =====
    function memoize(fn) {
      const cache = new Map()  // Map живёт в замыкании
    
      return function(...args) {
        const key = JSON.stringify(args)
    
        if (cache.has(key)) {
          return cache.get(key)
        }
    
        const result = fn.apply(this, args)
        cache.set(key, result)
        return result
      }
    }
    
    // Fibonacci без мемоизации: O(2^n)
    function fib(n) {
      if (n <= 1) return n
      return fib(n - 1) + fib(n - 2)
    }
    
    // С мемоизацией: O(n)
    const fastFib = memoize(function self(n) {
      if (n <= 1) return n
      return self(n - 1) + self(n - 2)
    })
    
    console.log('\nfib(10):', fastFib(10))   // 55
    console.log('fib(20):', fastFib(20))   // 6765
    console.log('fib(30):', fastFib(30))   // 832040
    
    // ===== 4. КЛАССИЧЕСКИЙ БАГ С var =====
    console.log('\n=== Баг с var в цикле ===')
    
    // БАГ
    const buggyFns = []
    for (var i = 0; i < 3; i++) {
      buggyFns.push(() => i)
    }
    // К этому моменту i = 3 (loop завершился)
    console.log('С var:', buggyFns.map(f => f()))  // [3, 3, 3]
    
    // ИСПРАВЛЕНИЕ с let
    const fixedFns = []
    for (let j = 0; j < 3; j++) {
      fixedFns.push(() => j)  // каждая итерация — своя переменная j
    }
    console.log('С let:', fixedFns.map(f => f()))  // [0, 1, 2]