← JavaScript/Замыкание#75 из 383← ПредыдущийСледующий →+35 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Замыкание

Реальная проблема: приватное состояние без классов

В React хук useState хранит значение между рендерами компонента — и это работает через замыкания. В Express.js middleware запоминает конфигурацию из момента создания. В любом кэше значение хранится «внутри» функции, недоступное снаружи. Замыкание — это механизм, на котором держится половина JS-экосистемы.

Что такое замыкание

Замыкание — это функция, которая помнит переменные из внешней области видимости даже после того, как внешняя функция завершила выполнение. Функция «захватывает» окружение в момент создания — как фотография.

На основе предыдущих уроков

  • «Функции» — функции как значения, возврат функции из функции
  • «Стрелочные функции» — стрелочные функции также создают замыкания
  • этот урок — основа для понимания урока 49 «Колбэки» и урока 52 «Каррирование»
  • Область видимости

    Переменные видны в той области, где объявлены, и во вложенных функциях:

    function outer() {
      const secret = 'я снаружи'
    
      function inner() {
        console.log(secret)  // inner ВИДИТ secret из outer
      }
    
      inner()  // 'я снаружи'
    }
    // secret здесь не видна

    Замыкание: функция «выживает» дольше своего родителя

    function makeCounter(start = 0) {
      let count = start  // эта переменная «захвачена»
    
      return function() {
        return ++count   // count живёт пока живёт возвращённая функция
      }
    }
    
    const counter = makeCounter(10)
    // outer (makeCounter) уже завершилась, но count = 10 помнится
    
    console.log(counter())  // 11
    console.log(counter())  // 12 — count в памяти!
    console.log(counter())  // 13

    Практические применения

    1. Приватное состояние:

    function createUser(name, email) {
      // эти переменные — «приватные», снаружи недоступны
      let _name = name
      let _email = email
      let _loginCount = 0
    
      return {
        login() {
          _loginCount++
          return `${_name} вошёл (всего: ${_loginCount} раз)`
        },
        getName: () => _name,
        changeName(newName) { _name = newName }
      }
    }
    
    const user = createUser('Алексей', 'alex@mail.ru')
    console.log(user.login())    // 'Алексей вошёл (всего: 1 раз)'
    console.log(user.login())    // 'Алексей вошёл (всего: 2 раз)'
    // _loginCount недоступен снаружи

    2. Фабричные функции — создание настроенных функций:

    function makeMultiplier(factor) {
      return (n) => n * factor  // factor захвачен
    }
    
    const double = makeMultiplier(2)
    const triple = makeMultiplier(3)
    
    console.log(double(5))  // 10
    console.log(triple(5))  // 15

    3. Мемоизация — кэширование результатов:

    function memoize(fn) {
      const cache = new Map()  // кэш захвачен замыканием
    
      return function(...args) {
        const key = JSON.stringify(args)
        if (cache.has(key)) {
          console.log('из кэша')
          return cache.get(key)
        }
        const result = fn(...args)
        cache.set(key, result)
        return result
      }
    }
    
    const expensiveCalc = memoize((n) => {
      // тяжёлые вычисления
      return n * n
    })
    
    console.log(expensiveCalc(10))  // вычисляет: 100
    console.log(expensiveCalc(10))  // из кэша: 100

    Типичные ошибки

    Ошибка 1: замыкание в цикле с var

    // Сломано — все коллбэки замыкают ОДНУ переменную i:
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100)
    }
    // 3, 3, 3 — не 0, 1, 2!
    
    // Исправлено — let создаёт новую переменную на каждую итерацию:
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100)
    }
    // 0, 1, 2

    Ошибка 2: утечка памяти через замыкание

    // Сломано: крупный массив остаётся в памяти пока живёт коллбэк
    function setup() {
      const bigData = new Array(1000000).fill('data')
      return function() {
        return bigData[0]  // bigData не освободится пока функция живёт
      }
    }
    
    // Исправлено: захватывай только то, что нужно
    function setup() {
      const bigData = new Array(1000000).fill('data')
      const firstItem = bigData[0]  // копируем нужное
      return function() {
        return firstItem  // bigData может быть собран GC
      }
    }

    Ошибка 3: замыкание «живёт» изменением, а не значением

    // Замыкание захватывает переменную, а не её значение в момент создания!
    let count = 0
    const getCount = () => count  // захвачена переменная count
    
    count = 42
    console.log(getCount())  // 42 — читает текущее значение!

    В реальных проектах

  • React hooks: useState и useEffect реализованы через замыкания
  • Debounce/throttle: замыкают таймер между вызовами
  • Middleware в Express: const authMiddleware = (role) => (req, res, next) => {...}
  • Модульный паттерн: скрытие внутренних деталей реализации (до появления ES-модулей)
  • Каррирование: создание специализированных функций
  • Примеры

    Счётчик запросов API с лимитом и мемоизация

    // Фабрика API-клиента с замыканием — приватное состояние
    function createApiClient(baseUrl, rateLimit = 10) {
      let requestCount  = 0
      let windowStart   = Date.now()
      const cache       = new Map()
    
      function checkRateLimit() {
        const now = Date.now()
        // Сброс окна каждую минуту
        if (now - windowStart > 60000) {
          requestCount = 0
          windowStart  = now
        }
        if (requestCount >= rateLimit) {
          throw new Error(`Превышен лимит: ${rateLimit} запросов/мин`)
        }
        requestCount++
      }
    
      // Мемоизированный fetch
      function cachedGet(path) {
        if (cache.has(path)) {
          console.log(`[кэш] GET ${path}`)
          return cache.get(path)
        }
        checkRateLimit()
        console.log(`[запрос ${requestCount}] GET ${baseUrl}${path}`)
        const result = { url: baseUrl + path, timestamp: Date.now() }
        cache.set(path, result)
        return result
      }
    
      return {
        get: cachedGet,
        getStats: () => ({ requestCount, cacheSize: cache.size }),
        clearCache: () => cache.clear(),
      }
    }
    
    const api = createApiClient('https://api.example.com', 5)
    
    api.get('/users')     // [запрос 1] GET https://api.example.com/users
    api.get('/products')  // [запрос 2] GET https://api.example.com/products
    api.get('/users')     // [кэш] GET /users — повторный запрос из кэша
    
    console.log(api.getStats())  // { requestCount: 2, cacheSize: 2 }
    
    // У каждого клиента своё состояние
    const api2 = createApiClient('https://other.com', 3)
    api2.get('/items')    // [запрос 1] GET https://other.com/items — независимый счётчик
    
    // Мемоизация тяжёлых вычислений
    function memoize(fn) {
      const cache = new Map()
      return function(...args) {
        const key = JSON.stringify(args)
        if (!cache.has(key)) cache.set(key, fn.apply(this, args))
        return cache.get(key)
      }
    }
    
    const fibonacci = memoize(function fib(n) {
      if (n <= 1) return n
      return fibonacci(n - 1) + fibonacci(n - 2)
    })
    
    console.log(fibonacci(40))  // 102334155 — быстро благодаря кэшу

    Замыкание

    Реальная проблема: приватное состояние без классов

    В React хук useState хранит значение между рендерами компонента — и это работает через замыкания. В Express.js middleware запоминает конфигурацию из момента создания. В любом кэше значение хранится «внутри» функции, недоступное снаружи. Замыкание — это механизм, на котором держится половина JS-экосистемы.

    Что такое замыкание

    Замыкание — это функция, которая помнит переменные из внешней области видимости даже после того, как внешняя функция завершила выполнение. Функция «захватывает» окружение в момент создания — как фотография.

    На основе предыдущих уроков

  • «Функции» — функции как значения, возврат функции из функции
  • «Стрелочные функции» — стрелочные функции также создают замыкания
  • этот урок — основа для понимания урока 49 «Колбэки» и урока 52 «Каррирование»
  • Область видимости

    Переменные видны в той области, где объявлены, и во вложенных функциях:

    function outer() {
      const secret = 'я снаружи'
    
      function inner() {
        console.log(secret)  // inner ВИДИТ secret из outer
      }
    
      inner()  // 'я снаружи'
    }
    // secret здесь не видна

    Замыкание: функция «выживает» дольше своего родителя

    function makeCounter(start = 0) {
      let count = start  // эта переменная «захвачена»
    
      return function() {
        return ++count   // count живёт пока живёт возвращённая функция
      }
    }
    
    const counter = makeCounter(10)
    // outer (makeCounter) уже завершилась, но count = 10 помнится
    
    console.log(counter())  // 11
    console.log(counter())  // 12 — count в памяти!
    console.log(counter())  // 13

    Практические применения

    1. Приватное состояние:

    function createUser(name, email) {
      // эти переменные — «приватные», снаружи недоступны
      let _name = name
      let _email = email
      let _loginCount = 0
    
      return {
        login() {
          _loginCount++
          return `${_name} вошёл (всего: ${_loginCount} раз)`
        },
        getName: () => _name,
        changeName(newName) { _name = newName }
      }
    }
    
    const user = createUser('Алексей', 'alex@mail.ru')
    console.log(user.login())    // 'Алексей вошёл (всего: 1 раз)'
    console.log(user.login())    // 'Алексей вошёл (всего: 2 раз)'
    // _loginCount недоступен снаружи

    2. Фабричные функции — создание настроенных функций:

    function makeMultiplier(factor) {
      return (n) => n * factor  // factor захвачен
    }
    
    const double = makeMultiplier(2)
    const triple = makeMultiplier(3)
    
    console.log(double(5))  // 10
    console.log(triple(5))  // 15

    3. Мемоизация — кэширование результатов:

    function memoize(fn) {
      const cache = new Map()  // кэш захвачен замыканием
    
      return function(...args) {
        const key = JSON.stringify(args)
        if (cache.has(key)) {
          console.log('из кэша')
          return cache.get(key)
        }
        const result = fn(...args)
        cache.set(key, result)
        return result
      }
    }
    
    const expensiveCalc = memoize((n) => {
      // тяжёлые вычисления
      return n * n
    })
    
    console.log(expensiveCalc(10))  // вычисляет: 100
    console.log(expensiveCalc(10))  // из кэша: 100

    Типичные ошибки

    Ошибка 1: замыкание в цикле с var

    // Сломано — все коллбэки замыкают ОДНУ переменную i:
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100)
    }
    // 3, 3, 3 — не 0, 1, 2!
    
    // Исправлено — let создаёт новую переменную на каждую итерацию:
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 100)
    }
    // 0, 1, 2

    Ошибка 2: утечка памяти через замыкание

    // Сломано: крупный массив остаётся в памяти пока живёт коллбэк
    function setup() {
      const bigData = new Array(1000000).fill('data')
      return function() {
        return bigData[0]  // bigData не освободится пока функция живёт
      }
    }
    
    // Исправлено: захватывай только то, что нужно
    function setup() {
      const bigData = new Array(1000000).fill('data')
      const firstItem = bigData[0]  // копируем нужное
      return function() {
        return firstItem  // bigData может быть собран GC
      }
    }

    Ошибка 3: замыкание «живёт» изменением, а не значением

    // Замыкание захватывает переменную, а не её значение в момент создания!
    let count = 0
    const getCount = () => count  // захвачена переменная count
    
    count = 42
    console.log(getCount())  // 42 — читает текущее значение!

    В реальных проектах

  • React hooks: useState и useEffect реализованы через замыкания
  • Debounce/throttle: замыкают таймер между вызовами
  • Middleware в Express: const authMiddleware = (role) => (req, res, next) => {...}
  • Модульный паттерн: скрытие внутренних деталей реализации (до появления ES-модулей)
  • Каррирование: создание специализированных функций
  • Примеры

    Счётчик запросов API с лимитом и мемоизация

    // Фабрика API-клиента с замыканием — приватное состояние
    function createApiClient(baseUrl, rateLimit = 10) {
      let requestCount  = 0
      let windowStart   = Date.now()
      const cache       = new Map()
    
      function checkRateLimit() {
        const now = Date.now()
        // Сброс окна каждую минуту
        if (now - windowStart > 60000) {
          requestCount = 0
          windowStart  = now
        }
        if (requestCount >= rateLimit) {
          throw new Error(`Превышен лимит: ${rateLimit} запросов/мин`)
        }
        requestCount++
      }
    
      // Мемоизированный fetch
      function cachedGet(path) {
        if (cache.has(path)) {
          console.log(`[кэш] GET ${path}`)
          return cache.get(path)
        }
        checkRateLimit()
        console.log(`[запрос ${requestCount}] GET ${baseUrl}${path}`)
        const result = { url: baseUrl + path, timestamp: Date.now() }
        cache.set(path, result)
        return result
      }
    
      return {
        get: cachedGet,
        getStats: () => ({ requestCount, cacheSize: cache.size }),
        clearCache: () => cache.clear(),
      }
    }
    
    const api = createApiClient('https://api.example.com', 5)
    
    api.get('/users')     // [запрос 1] GET https://api.example.com/users
    api.get('/products')  // [запрос 2] GET https://api.example.com/products
    api.get('/users')     // [кэш] GET /users — повторный запрос из кэша
    
    console.log(api.getStats())  // { requestCount: 2, cacheSize: 2 }
    
    // У каждого клиента своё состояние
    const api2 = createApiClient('https://other.com', 3)
    api2.get('/items')    // [запрос 1] GET https://other.com/items — независимый счётчик
    
    // Мемоизация тяжёлых вычислений
    function memoize(fn) {
      const cache = new Map()
      return function(...args) {
        const key = JSON.stringify(args)
        if (!cache.has(key)) cache.set(key, fn.apply(this, args))
        return cache.get(key)
      }
    }
    
    const fibonacci = memoize(function fib(n) {
      if (n <= 1) return n
      return fibonacci(n - 1) + fibonacci(n - 2)
    })
    
    console.log(fibonacci(40))  // 102334155 — быстро благодаря кэшу

    Задание

    Реализуй функцию createRateLimiter(maxCalls, windowMs), которая возвращает функцию-обёртку. Обёртка принимает любую функцию fn и возвращает новую функцию, которая вызывает fn не чаще maxCalls раз за windowMs миллисекунд. Если лимит превышен — выбрасывает ошибку 'Rate limit exceeded'. Используй замыкание для хранения счётчика и времени.

    Подсказка

    Инициализируй calls = 0, windowStart = Date.now(). Условие сброса: now - windowStart >= windowMs. После сброса: calls = 0, windowStart = now. Затем проверь calls >= maxCalls, увеличь calls++ и вызови fn(...args).

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