Замыкания в JavaScript
Замыкание — это комбинация функции и лексической области видимости, в которой эта функция была объявлена.
Если сейчас из этого определения ничего не понятно — не расстраивайтесь. В статье я шаг за шагом расскажу про каждый термин, покажу примеры, дам практические упражнения.
Область видимости в JavaScript
Область видимости (scope)— это зона доступности переменной, функции или объекта.
Если вы положите сумку с деньгами посреди центральной площади своего города, она окажется в области видимости всех прохожих. Если вы положите её на журнальный столик в гостиной — сумка окажется в области видимости жильцов вашей квартиры.
Аналогично и с переменной (сумка), которая хранит какое то значение (сумма денег):
const moneyBag = 1000;
console.log(moneyBag); // 1000
Мы создали переменную на самом верхнем уровне в глобальной области видимости. Это значит, что доступ к ней будет доступен из любой части программы (все прохожие имеют доступ к сумке с деньгами).
Область видимости в JS может быть глобальной, функциональной и блочной.
В области видимости котейки — все, что снаружи (глобальная область видимости), и то, что внутри коробки (функциональная или блочная область видимости)
Пример того, как работает разная область видимости:
// глобальная область видимости
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))
Как решить эту задачу с помощью замыкания:
- В функции inBetween обязательно нужно вернуть колбэк функцию, которую поймет метод .filter()
- Значения, которые мы передадим в качестве аргументов функции 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 и как его использовать, рекомендую проверить и закрепить понимание на практике:
- Создать замыкание, когда при вызове функция выводит ваше имя в консоль.
- Написать функцию inArray, которая выбирает только элементы, совпадающие с одним из элементов массива.
[15, 1, 27, 3, 5].filter(inArray([1,2,3])) // [1,3]
Итог
- Функция имеет доступ к данным, указанным в её теле, внутри родительской функции. Это лексическая область видимости.
- Замыкание — когда мы храним или используем функцию вместе с её лексической области видимости на момент вызова.
- Замыкание помогает контролируемо ограничивать область видимости и повторно использовать логику в коде.