← Собеседование/Что такое замыкание? Покажи на примере.#368 из 383← ПредыдущийСледующий →+40 XP
Полезно по теме:Маршрут: подготовка к интервьюГайд: портфолио juniorГайд: карьерный планТермин: Closure
← НазадДалее →

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

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

Замыкание — это функция вместе с лексическим окружением, в котором она была создана. Проще говоря: внутренняя функция "помнит" переменные внешней функции даже после того, как внешняя функция завершила выполнение. Замыкания — фундаментальный механизм 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]

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

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

    Замыкание — это функция вместе с лексическим окружением, в котором она была создана. Проще говоря: внутренняя функция "помнит" переменные внешней функции даже после того, как внешняя функция завершила выполнение. Замыкания — фундаментальный механизм 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]

    Задание

    Исправь классический баг с замыканием в цикле, а затем реализуй функцию createLogger — фабрику логгеров с приватным префиксом и счётчиком сообщений.

    Подсказка

    Для исправления бага замени var на let. Для createLogger: все методы замкнуты на переменную count. warn увеличивает тот же счётчик. getCount() возвращает count. reset() устанавливает count = 0.

    Загружаем среду выполнения...
    Загружаем AI-помощника...