DevSurge 💦

Замыкания в JavaScript

Cover Image for Замыкания в JavaScript
Mark Nelyubin
Mark Nelyubin
Замыкание — это комбинация функции и лексической области видимости, в которой эта функция была объявлена.

Если сейчас из этого определения ничего не понятно — не расстраивайтесь. В статье я шаг за шагом расскажу про каждый термин, покажу примеры, дам практические упражнения.

Область видимости в JavaScript

Область видимости (scope)— это зона доступности переменной, функции или объекта.

Если вы положите сумку с деньгами посреди центральной площади своего города, она окажется в области видимости всех прохожих. Если вы положите её на журнальный столик в гостиной — сумка окажется в области видимости жильцов вашей квартиры.

Аналогично и с переменной (сумка), которая хранит какое то значение (сумма денег):

const moneyBag = 1000;
console.log(moneyBag); // 1000

Мы создали переменную на самом верхнем уровне в глобальной области видимости. Это значит, что доступ к ней будет доступен из любой части программы (все прохожие имеют доступ к сумке с деньгами).

Область видимости в JS может быть глобальной, функциональной и блочной.

Article Image

В области видимости котейки — все, что снаружи (глобальная область видимости), и то, что внутри коробки (функциональная или блочная область видимости)

Пример того, как работает разная область видимости:

// глобальная область видимости
const moneyBagAmount = 1000;
console.log(moneyBagAmount); // 1000

// функциональная область видимости
function logMoney(){
  console.log(moneyBagAmount); 
}

logMoney(); // 1000 

// блочная область видимости
if (true) {
  console.log(moneyBagAmount); // 1000
} 

const account = {
  amount: moneyBagAmount,
}
console.log(account.amount) // 1000

Блочная область видимости ограничена программным блоком, который обозначается фигурными скобками { }. Если мы объявим переменную в блоке, то доступ к ней будет внутри этого блока (и его дочерних блоков), но не снаружи (глобально);

const moneyBagAmount = 6000;

if (moneyBagAmount > 0){
  const moneyInUsd = moneyBagAmount/60;
  // можем прочитать значение переменной внутри этого блока (или вложенных в него)
  console.log(moneyInUsd); // 100
}

// можем прочитать снаружи значение переменной из глобальной области видимости
console.log(moneyBagAmount); // 6000
// не можем прчитать значение переменной из блочной области видимости, она не объявлена глобально
console.log(moneyInUsd); // Uncaught ReferenceError: moneyInUsd is not defined

Функциональная область видимости — это то, что находится в пределах тела функции. Тело функции обозначается фигурными скобками { }.

const moneyBagAmount = 1000;

function convertToUsd(amount){
  const dollars = amount / 60;
  // округляем до 2 знаков после запятой
  const result = Number(parseFloat(dollars).toFixed(2));
  return result;
}

// result недоступен снаружи, так как объявлен в функциональной области видимости
console.log(result); // Uncaught ReferenceError: result is not defined

// к счастью, функция возвращает значение result, мы получим его при вызове
convertToUsd(moneyBagAmount); // 16.67

Для функции доступны данные, которые находятся в глобальной области видимости, в её собственной области видимости и в теле родительской функции. Доступ к локальным переменным родительской функции называют лексической областью видимости.

const moneyBagAmount = 1000;

function parent() {
  const currencies = ['usd', 'eur', 'rub'];

  function child() {
    // доступно чтение значения переменной, объявленной глобально
    console.log(moneyBagAmount); // 1000
    // доступно значение переменной, объявленной в области видимости родительской функции
    console.log(currencies); // ['usd', 'eur', 'rub'];
  }

  child();
}

parent(); 

Это всё очень интересно, но при чем тут замыкание? Сейчас всё станет понятно.

Замыкание в JavaScript

Вспомним определение, которое написано в самом начале:

Замыкание — это комбинация функции и лексической области видимости, в которой эта функция была объявлена.

И разберем на упрощенном примере, который иллюстрировал лексическую область видимости:

function parent() {
  // лексическая область видимости для child - все, что в теле {} функции parent
  const currencies = ['usd', 'eur', 'rub'];
  
  // функция child объявлена в той же области видимости, что и currencies и имеет к ней доступ
  function child() {
    console.log(currencies);
  }
}

У нас есть функция child, есть лексическая область видимости, не хватает только их комбинации. Чтобы создать комбинацию функции и лексической области видимости, родительская функция должна возвращать дочернюю:

function parent() {
  const currencies = ['usd', 'eur', 'rub'];
  
  // функцию нужно вернуть
  return function child() {
    return currencies;
  }
}

/* сохраняем в closure значение currencies на момент вызова 
и функцию child, которую возвращает parent() */
const closure = parent(); 
closure(); // ['usd', 'eur', 'rub']

Поздравляю, мы только что создали замыкание. Мы сохранили в переменную closure функцию child вместе с её лексической областью видимости на момент вызова (переменная currencies и ее значение).

В разные моменты вызова данные в лексической области видимости нашей функции могут отличаться:

let num = 1;

function parent(val){
    return function child(){
        console.log({val, num});
    }
}

const closure1 = parent('closure 1 call');
const closure2 = parent('closure 2 call'); // в области видимости closure2 будет другое значение val

closure1(); // {val: 'closure 1 call', num: 1}
                        
num++;
/* на момент вызова closure 2 в лексической области видимости другое значение val и num */
closure2(); // {val: 'closure 2 call', num: 2}
Заметка по поводу синтаксиса. Помимо декларации функции, можем использовать функциональное выражение и стрелочную функцию для создания замыкания
function parent() {
  const currencies = ['usd', 'eur', 'rub'];
  // именованная функция
  return function child() {
    return currencies;
  }

  // анонимная функция
  return function(){
    return currencies
  }
  
  // функциональное выражение
  const child = function(){return currencies};
  return child;
  
  // сокращенная стрелочная функция
  return () => currencies 
  
  // именованная стрелочная функция
  const child = () => currencies;
  return child;
}

Примеры использования замыкания в JS

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

const adder = x => y => x+y

const add100 = adder(100); // add100 возвращает функцию y => 100+y
add100(50); // вызываем функцию 50=>100+50, получаем 150

С помощью замыкания можно создать счетчик, где мы контролируем то, как меняется значение. Мы можем использовать этот функционал в разных компонентах независимо друг от друга:

const counter = (count = 0) => {
  const increase = () => count++;
  const decrease = () => count--;  
  const getValue = () => count;
  
  // можем вернуть несколько функций, у которых есть доступ к count
  return {increase, decrease, getValue}
}

/* создаем замыкание с дефолтным значением count=0
componentCounter содержит объект с функциями, которые читают 
или меняют значение count: {increase: ƒ, decrease: ƒ, getValue: ƒ}
*/ 
const componentCounter = counter();

// увеличиваем счетчик на 3
componentCounter.increase()
componentCounter.increase()
componentCounter.increase()

// читаем значение count
componentCounter.getValue() // 3

Следующий пример про создание заготовленных фильтров с помощью замыкания и метода .filter(). Сначала напомню, как работает этот метод:

/* фильтр принимает колбэк функцию, которая возвращает true или false
   проходится по всем элементам. Если элемент соответстует условию 
   и колбэк возвращает true, тогда элемент попадет в новый массив */
const filteredData = [1,2,3].filter(x => x>2);
console.log(filteredData); // [2, 3]

Допустим, мы хотим, чтобы фильтр возвращал нам все цифры от 1 до 10 включительно:

console.log([4, 135,18,37, 9].filter((x) => x>=1 && x<=10)); // [4,9]

Что если в одном компоненте нам нужно фильтровать от 1 до 10, в другом — от 1 до 99, в третьем — от 113 до 117. Каждый раз придется писать новую колбэк функцию? Можно упростить повторное использование с помощью замыкания, чтобы значения диапазона мы могли просто передать в качестве аргументов:

arr.filter(inBetween(1, 10))

Как решить эту задачу с помощью замыкания:

  1. В функции inBetween обязательно нужно вернуть колбэк функцию, которую поймет метод .filter()
  2. Значения, которые мы передадим в качестве аргументов функции inBetween должны быть в лексической области видимости возвращаемой колбэк функции

Решение будет выглядеть следующим образом:

const arr = [4, 135,18,37, 9];

/* принимает значения min и max, когда разработчик передает их в качестве аргументов при вызове. Возвращает колбек функцию, где используются min и max для сравнения */
const inBetween = (min, max) => {
  return (x) => x>=min && x<=max
}

// когда мы вызываем inBetween(1,10) она вернет (x)=> x>=1 && x<=10
// метод .filter((x)=> x>=1 && x<=10) вернет значения от 1 до 10
const result = arr.filter(inBetween(1, 10)); 
console.log(result); // [4, 9]

Если вы поняли, что такое замыкание в JavaScript и как его использовать, рекомендую проверить и закрепить понимание на практике:

  1. Создать замыкание, когда при вызове функция выводит ваше имя в консоль.
  2. Написать функцию inArray, которая выбирает только элементы, совпадающие с одним из элементов массива.[15, 1, 27, 3, 5].filter(inArray([1,2,3])) // [1,3]

Итог

  • Функция имеет доступ к данным, указанным в её теле, внутри родительской функции. Это лексическая область видимости.
  • Замыкание — когда мы храним или используем функцию вместе с её лексической области видимости на момент вызова.
  • Замыкание помогает контролируемо ограничивать область видимости и повторно использовать логику в коде.

Другие материалы

Cover Image for TypeScript Utility Types — вспомогательные типы и области их применения

TypeScript Utility Types — вспомогательные типы и области их применения

что такое Utility Types в TypeScript, расскажу про основные вспомогательные типы, и покажу, как применять их на реальных проектах.

Mark Nelyubin
Mark Nelyubin
Cover Image for Принципы SOLID с примерами на JS и Vue

Принципы SOLID с примерами на JS и Vue

Расскажу про принципы SOLID с актуальными примерами на JavaScript, Vue, React.

Mark Nelyubin
Mark Nelyubin