Область видимости переменных и замыкания — это ключевые концепции в программировании, которые важно понимать для эффективного написания кода. Область видимости определяет, где переменные доступны в программе, а замыкания позволяют сохранять состояние функции даже после того, как она завершила выполнение. В этой статье мы рассмотрим эти концепции более подробно и их практическое применение в JavaScript.
Область видимости
Область видимости (Scope) — это часть программы, в которой мы можем обратиться к переменной, функции или объекту. Этой частью может быть функция, блок или вся программа в целом — то есть мы всегда находимся как минимум в одной области видимости.1
Область видимости определяет, где переменные доступны и где недоступны в программе. Под доступностью подразумевается, что можно считывать и изменять переменные.
Типы областей видимости
Область видимости может быть глобальной (Global Scope) или локальной (Local Scope). В свою очередь, локальная область видимости может быть функциональной (Function Scope) или блочной (Block Scope).
Каждый блок кода или функция создает свою собственную область видимости. Это означает, что переменная, объявленная внутри блока или функции, доступна только внутри этого блока или функции.
Глобальная
Переменные, объявленные вне функций, находятся в глобальной области видимости и могут быть доступны в любой части программы.
// Глобальная область видимости
// Инициализация глобальной переменной message
const message = "Привет!";
function sayHello() {
console.log(message);
}
sayHello(); // Привет!
В приведенном выше примере переменная message объявлена в глобальной области и доступна в локальной области функции sayHello.
Сразу следует заметить, что необдуманное применение глобальных переменных приводит к негативным последствиям:
- Сложность отладки. Использование глобальных переменных делает сложным отслеживание и понимание того, где и как эти переменные изменяются в коде. Это может затруднить процесс отладки и исправления ошибок.
- Нежелательные побочные эффекты. Поскольку глобальные переменные могут быть доступны из любой части программы, изменения, внесенные в них в одном месте, могут непредсказуемо повлиять на другие части программы.
- Затруднение масштабирования. При разработке больших проектов глобальные переменные могут привести к сложностям в управлении состоянием приложения и его масштабируемостью.
- Затруднение повторного использования. Использование глобальных переменных может создать зависимости между различными частями кода, что может затруднить их повторное использование в других проектах или контекстах.
- Безопасность. Глобальные переменные могут представлять угрозу безопасности, так как они могут быть доступны и изменены из различных участков программы без должного контроля и проверок.
Функциональная область видимости
Переменные, объявленные внутри функции, находятся в функциональной области видимости. Это означает, что они доступны только внутри этой функции и вложенных функций.
Каждая функция создает свою собственную область видимости.
Переменные могут иметь одно и то же имя в разных функциях, поскольку локальные переменные недоступны из других функций.
// Глобальная область видимости
// Инициализация глобальной переменной message
const message = "До встречи";
// Функциональная область видимости
function sayHello() {
// Инициализация функциональных переменных message и myName
const message = "Привет"
const myName = "Семён";
console.log(`${message}, ${myName}!`)
}
// Функциональная область видимости
function sayQuote() {
// Инициализация функциональной переменной message
const message = "Куй железо не отходя от кассы!";
console.log(message);
}
console.log(message); // До встречи
sayHello(); // Привет, Семён!
sayQuote(); // Куй железо не отходя от кассы!
Когда программа выполняется, она ищет переменные сначала в текущей области видимости, затем во внешних областях видимости. Если переменная не найдена, может возникнуть ошибка.
Переменная, объявленная внутри функции, недоступна извне.
Блочная область видимости
В JavaScript также существует блочная область видимости (Block Scope) с появлением введения let и const в ES6. Переменные, объявленные с помощью let и const, ограничены блоком кода (например, внутри if, for или while) и недоступны за его пределами.
// Глобальная область видимости
const globalVar = "Global";
function functionalScope() {
// Функциональная область видимости
const functionVar = "Function";
if (true) {
// Блочная область видимости
const blockVar = "Block";
console.log(globalVar); // Global
console.log(functionVar); // Function
console.log(blockVar); // Block
}
// Переменная blockVar не доступна здесь
console.log(globalVar); // Global
console.log(functionVar); // Function
console.log(blockVar); // Ошибка ReferenceError
}
functionalScope();
В примере выше переменная globalVar доступна везде, так как она объявлена в глобальной области видимости. Переменная functionVar доступна только внутри функции functionalScope, так как она объявлена в функциональной области видимости. Переменная blockVar доступна только в блочной области видимости внутри условного оператора if.
Выводы
- Каждая область видимости имеет доступ к переменным, которые были объявлены в родительских областях.
- Если вы правильно определите область действия своих переменных, вы обнаружите, что ваш код легче читать и отлаживать.
- Область видимости помогает избежать конфликтов и нежелательного изменения значений переменных в разных частях программы, обеспечивая четкую организацию кода и упрощение его понимания.
Автор книги «Новые возможности JavaScript. Как написать чистый код по всем правилам современного языка» Ти Джей Краудер дает такой совет:
Сохраняйте узкую область видимости переменных. … Используйте let и const в максимально узкой подходящей области, где это разумно. Это повышает удобство обслуживания кода.2
Лексическая область видимости и замыкания
Понимание тонкостей лексической области видимости и замыканий в JavaScript поможет избежать ошибок и писать более качественный код.
Лексическая область видимости
Как и в большинстве современных языков программирования, в JavaScript используется лексическая область видимости.
Лексическая область видимости (Lexical Scope) — это конкретный механизм, одно из правил для области видимости. Под лексической областью видимости можно понимать просто механизм поиска значений: смотрим в текущей области, если нет — идём на уровень выше, и так далее. Слово «лексический» означает, что видимость задаётся исключительно текстом программы, исходным кодом. То есть можно смотреть на программу, не запуская её, и понять область видимости в любой точке.3
Может возникать путаница между областью видимости и лексической областью видимости из-за схожести этих понятий, но они имеют разное значение.
- Область видимости определяет, где переменная доступна в программе.
- Лексическая область видимости определяет, где искать значения переменных во время выполнения программы, основываясь на их расположении в исходном коде.
В языках программирования с лексической областью видимости переменные доступны внутри блока, в котором они были определены, а также во всех вложенных блоках.
Рассмотрим простой пример:
const a = 5;
const b = 10;
function sum() {
const a = 7;
return a * b;
}
console.log(sum()); // 70
Функция sum возвращает сумму a и b.
Из функции sum доступна как переменная a объявленная в локальной области видимости, так и переменная a объявленная в глобальной области видимости. Значение какой переменной будет использовано при сложении?
Поиск переменной a начнется с локальной области видимости, где переменная и будет найдена — на этом поиск закончится. Движок нашел под рукой то, что необходимо, поэтому нет необходимости искать дальше.
После того как переменную b не удастся найти в локальной области видимости, поиск продолжится в наружной области, которая в нашем примере является глобальной.
Значение переменной a равно 7, значение переменой b равно 10 — в консоли получим 70.
Поскольку лексическая область видимости не изменяется во время выполнения программы JavaScript, её еще называют статической областью видимости.
Полагаю, что тему лексической области видимости имеет смысл изучать на контрасте с динамической областью видимости. И это несмотря на то, что в JavaScript нет динамической области видимости. Дело в том, что может показаться, что лексическая область видимости — это что-то само собой разумеющееся и безальтернативное. Поэтому рекомендую заглянуть в книгу Кайла Симпсона «Замыкания и объекты» из серии «Вы не знаете JS».4
Замыкания (Closures)
Ниже предложены несколько определений замыканий из разных источников.
Замыкание — это комбинация функции и лексического окружения вместе со всеми доступными внешними переменными. В JavaScript замыкания создаются каждый раз при создании функции, во время ее создания.5
Замыкание = функция + внешний контекст
Замыкание — это функция, вложенная во внешнюю функцию, которая сохраняет доступ к переменным, объявленным во внешней функции.6
Ниже простой пример.
// Внешняя функция
function outer() {
let message = "Hello";
// Внутренняя функция inner имеет доступ ко всему, что находится внутри
// внешней функции, включая переменную message
function inner() {
console.log(message);
}
inner();
}
outer();
Все, что находится внутри внешней (родительской) функции, является частью замыкания.
Библиотеки и фреймворки JavaScript такие как React, Vue и Angular постоянно используют замыкания.
«Счетчик» — разберемся, чем полезны замыкания
Разобраться в замыканиях поможет простая задача.
Создать функцию-счётчик, которая считает свои вызовы и возвращает их текущее число.
Реализация с использованием глобальной переменной count
Реализация с использованием глобальной переменной count может выглядеть следующим образом:
let count = 0; // Глобальная переменная count
function counter() {
count++;
return count;
}
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Использование глобальной переменной для хранения состояния счетчика может в дальнейшем привести к проблемам, ведь её значение можно изменить из любой части программы.
let count = 0; // Глобальная переменная count
function counter() {
count++;
return count;
}
count = 100;
console.log(counter()); // 101
console.log(counter()); // 102
console.log(counter()); // 103
count = "Hello";
console.log(counter()); // NaN
console.log(counter()); // NaN
console.log(counter()); // NaN
Как видно из примера выше, функция работает непредсказуемо из-за внешних воздействий.
Реализация с использованием локальной переменной count
Необходимо инкапсулировать (защитить, изолировать) функцию от внешнего воздействия.
Согласно принципу минимальных привилегий7, следует предоставлять доступ только к тем переменным, которые абсолютно необходимы, и скрывать все остальные. В нашем случае нет никакой необходимости в том, чтобы переменная count была доступно глобально.
Если мы просто перенесем глобальную переменную count внутрь функции counter, то переменная станет локальной. Однако программа будет работать неправильно, так как переменная count обнуляется каждый раз при вызове функции counter.
function counter() {
let count = 0; // Локальная переменная count
count++;
return count;
}
console.log(counter()); // 1
console.log(counter()); // 1
console.log(counter()); // 1
Получается, что для реализации рабочей функции без использования глобальной переменной, недостаточно перенести count в локальную область видимости.
Потребуется внешняя функция, которая создаст функцию счетчика и сохранит внешний контекст (данные, необходимые для работы функции) защищенный от воздействий извне.
function createCounter() {
// Переменная count существует до тех пор, пока
// существует ссылка на функцию createCounter
let count = 0;
function increment() {
count++;
return count;
}
return increment;
}
// Функция createConter вернет функцию increment,
// которая станет значением переменной counter
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// Будем искать замыкание
console.log(counter.prototype);
При помощи замыкания удалось инкапсулировать функцию и переменную count. Поменять значение переменной из внешнего кода теперь невозможно. При этом у нас теперь есть функция increment в глобальной области видимости — она записана в переменную counter.
Как это работает?
Для работы счетчика необходима переменная count, но ведь после того, как функция createCounter завершит выполнение, переменная перестанет существовать. Где же сохраняется её значение?
В книге Криса Минника и Евы Холланд «JavaScript для чайников» дано несколько упрощенное определение замыкания:
Замыкание — это локальная переменная функции, которая сохраняет свое значение даже после того, как из функции был выполнен возврат.8
Как нам «увидеть» замыкание? Для этой цели в коде программы предусмотрена строка console.log(counter.prototype), которая позволит изучить свойство prototype функции.
Родительская функция createCounter вернула не только функцию increment, но и скрытый объект, в котором хранится все, что необходимо для её работы.
Вспоминаем:
Замыкание = функция + внешний контекст
Использование стрелочных функций
Внешнюю и внутреннюю функции можно написать используя стрелочный синтаксис. Внутреннюю функцию логично сделать анонимной (безымянной), сделав код более лаконичным.
Немного усложним пример, добавив возможность изменять начальное состояние счетчика — в качестве аргумента будем передавать целое число.
const createCounter = (init) => {
let count = init;
return () => ++count;
}
const counter = createCounter(10);
console.log(counter()); // 11
console.log(counter()); // 12
console.log(counter()); // 13
console.log(counter.prototype); // undefined
console.dir(counter); // anonymous()
У стрелочных функций нет свойств сonstructor и prototype, поэтому в строке 12 получаем undefined. Можно использовать console.dir() — вывод всех свойств объекта в виде разворачивающегося древовидного списка.
Замыкание сохраняет состояние переменной и делает её недоступной извне.
Замыкание в обработчике
На YouTube нашел пример неочевидного для меня использования замыкания.
Мой код будет несколько отличаться от кода в видео, но это не меняет сути.
Содержимое HTML-файла ниже. Три кнопки с идентификаторами: red, green и blue.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Область видимости и замыкания в JS</title>
</head>
<body>
<h1>Замыкание</h1>
<button id="red">Red</button>
<button id="green">Green</button>
<button id="blue">Blue</button>
<script src="script.js"></script>
</body>
</html>
Поскольку кнопок может быть очень много, для избежания дублирования кода, логика вынесена в отдельную функцию-обработчик changeColor. В качестве аргумента функция принимает конкретный цвет.
Чтобы функция не срабатывала сразу после открытия веб-страницы, приходится трижды оборачивать её в безымянную функцию:
const redBtn = document.querySelector("#red");
const greenBtn = document.querySelector("#green");
const blueBtn = document.querySelector("#blue");
const body = document.querySelector("body");
function changeColor(color) {
body.style.backgroundColor = color;
}
redBtn.addEventListener("click", function () {
changeColor("red");
});
greenBtn.addEventListener("click", function () {
changeColor("green");
});
blueBtn.addEventListener("click", function () {
changeColor("blue");
});
Избежать ненужного дублирования кода позволит замыкание:
const redBtn = document.querySelector("#red");
const greenBtn = document.querySelector("#green");
const blueBtn = document.querySelector("#blue");
const body = document.querySelector("body");
function changeColor(color) {
return function () {
body.style.backgroundColor = color;
};
}
redBtn.addEventListener("click", changeColor("red"));
greenBtn.addEventListener("click", changeColor("green"));
blueBtn.addEventListener("click", changeColor("blue"));
Код можно еще немного сократить, используя стрелочный синтаксис:
const changeColor = (color) => () => body.style.backgroundColor = color;
Совет от Симпсона — взгляд в другом направлении
Есть очень веская причина того, что у нас есть замыкания в JavaScript. Существует проблема, и мы пытаемся решить её с помощью замыканий. Мне показался очень полезным совет от Кайла Симпсона:
Традиционный подход к функциям подразумевает, что вы объявляете функцию, а потом добавляете в нее код. Однако не менее полезно взглянуть в другом направлении: вы берете произвольный фрагмент кода, написанный вами, и заключаете его в объявление функции, что фактически «скрывает» код от наблюдателя9.
Задачи на замыкания с LeetCode
Для закрепления темы рекомендую решить задачи с leetcode.com:
Ссылки
- Области видимости. Сайт doka.guide ↩︎
- Краудер Т. Новые возможности JavaScript. Как написать чистый код по всем правилам современного языка. Москва: Бомбора, 2023. — С. 66 — ISBN 978-5-04-159515-9 ↩︎
- Окружение. Сайт hexlet.io ↩︎
- Симпсон К. Вы не знаете JS: Замыкания и объекты. Санкт-Петербург: Питер, 2024. — С. 115. — ISBN 978-5-4461-2314-8. ↩︎
- МакГрат М. JavaScript для начинающих. 6-е издание, Москва: Бомбора, 2023 — С. 29 — ISBN 978-5-04-121621-4. ↩︎
- МакГрат М. JavaScript для начинающих. 6-е издание, Москва: Бомбора, 2023 — С. 32 — ISBN 978-5-04-121621-4. ↩︎
- Принцип минимальных привилегий. Сайт wikipedia.org ↩︎
- Минник К, Холланд Е. JavaScript для чайников. Москва: Диалектика-Вильямс, 2020 — С. 224 — ISBN 978-5-907144-47-7. ↩︎
- Симпсон К. Вы не знаете JS: Замыкания и объекты. Санкт-Петербург: Питер, 2024. — С. 59 — ISBN 978-5-4461-2314-8. ↩︎
Оставьте первый комментарий