Добавить в корзинуПозвонить
Найти в Дзене
ovnoCod

Язык JavaScript - Область видимости переменных, замыкание

Вы когда-нибудь задумывались, почему переменная, объявленная внутри функции, не видна снаружи? Или почему функция "запоминает" переменные из внешней функции даже после того, как та завершила работу? Добро пожаловать в мир областей видимости и замыканий - фундаментальных концепций, которые делают JavaScript таким гибким и мощным. Это не просто теория. Это знание, которое отделяет новичка, случайно создающего глобальные переменные, от профессионала, пишущего чистый, предсказуемый и эффективный код. Сегодня мы разберём, где живут переменные, как они умирают и как функция может "украсть" переменную из другой функции и удерживать её вечно. Область видимости - это территория, на которой переменная доступна. Это как правила дорожного движения для ваших переменных: где они видны, где их можно использовать, а где они исчезают. javascript // Глобальная область (видна всем)
const globalVar = "Я виден везде";
function myFunction() {
// Локальная область (видна только внутри функции)
const loc
Оглавление

Территории памяти: Как работает область видимости и замыкания в JavaScript

Вы когда-нибудь задумывались, почему переменная, объявленная внутри функции, не видна снаружи? Или почему функция "запоминает" переменные из внешней функции даже после того, как та завершила работу?

Добро пожаловать в мир областей видимости и замыканий - фундаментальных концепций, которые делают JavaScript таким гибким и мощным. Это не просто теория. Это знание, которое отделяет новичка, случайно создающего глобальные переменные, от профессионала, пишущего чистый, предсказуемый и эффективный код.

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

Часть 1. Что такое область видимости (Scope)?

Область видимости - это территория, на которой переменная доступна. Это как правила дорожного движения для ваших переменных: где они видны, где их можно использовать, а где они исчезают.

javascript

// Глобальная область (видна всем)
const globalVar = "Я виден везде";

function myFunction() {
// Локальная область (видна только внутри функции)
const localVar = "Я виден только внутри myFunction";
console.log(globalVar);
// ✅ Доступно
console.log(localVar);
// ✅ Доступно
}

console.log(globalVar);
// ✅ Доступно
console.log(localVar);
// ❌ ReferenceError: localVar is not defined

Часть 2. Три короля областей видимости

2.1 Глобальная область (Global Scope)

Переменные, объявленные вне любой функции или блока, живут в глобальной области. Они доступны отовсюду.

javascript

// Глобальные переменные
let globalLet = "видна везде";
var globalVar = "тоже видна везде";
const GLOBAL_CONST = "константа";

function showGlobal() {
console.log(globalLet);
// ✅
console.log(globalVar);
// ✅
console.log(GLOBAL_CONST);
// ✅
}

// В браузере глобальные переменные становятся свойствами window
console.log(window.globalVar);

⚠️ Опасность: Слишком много глобальных переменных - это загрязнение глобального пространства. Они могут конфликтовать, перезаписываться и приводить к трудноуловимым багам.

2.2 Функциональная область (Function Scope)

var имеет функциональную область видимости. Переменная видна во всей функции, независимо от того, где в функции она объявлена (благодаря hoisting).

javascript

function functionalScope() {
if (true) {
var insideIf = "я виден во всей функции";
}
console.log(insideIf);
// ✅ "я виден во всей функции"

for (var i = 0; i < 3; i++) {
// i видна после цикла
}
console.log(i);
// ✅ 3
}

// console.log(insideIf); // ❌ ReferenceError (вне функции)

2.3 Блочная область (Block Scope)

let и const имеют блочную область видимости. Блок - это всё, что находится между { }.

javascript

function blockScope() {
if (true) {
let insideIf = "я виден только внутри if";
const alsoInside = "тоже только внутри if";
}
// console.log(insideIf); // ❌ ReferenceError

for (let i = 0; i < 3; i++) {
// i существует только внутри цикла
}
// console.log(i); // ❌ ReferenceError
}

Часть 3. Вложенные области и цепочка областей видимости

Когда JavaScript ищет переменную, он идёт по цепочке: от самой внутренней области наружу, до глобальной.

javascript

const animal = "слон"; // глобальная

function outer() {
const animal = "тигр";
// область outer

function inner() {
const animal = "кот";
// область inner
console.log(animal);
// "кот" (нашёл в своей области)
}

inner();

function inner2() {
console.log(animal);
// "тигр" (не нашёл в inner2, поднялся в outer)
}

inner2();
}

outer();
console.log(animal);
// "слон" (глобальная)

Правило: JavaScript ищет переменную в текущей области. Если не находит - поднимается на уровень выше. И так до глобальной области. Если и там нет - ошибка (в строгом режиме) или создание глобальной переменной (в нестрогом, что очень плохо).

Часть 4. Замыкание (Closure) - магия JavaScript

Замыкание - это способность функции "запоминать" переменные из внешней области видимости даже после того, как внешняя функция завершила выполнение.

4.1 Простейший пример замыкания

javascript

function createCounter() {
let count = 0;
// переменная из внешней функции

return function() {
count++;
// внутренняя функция "запоминает" count
return count;
};
}

const counter = createCounter();
console.log(counter());
// 1
console.log(counter());
// 2
console.log(counter());
// 3

// Переменная count продолжает жить внутри counter!
// Она не видна снаружи
console.log(count);
// ❌ ReferenceError

Что произошло? Функция, возвращённая из createCounter, создала замыкание. Она "захватила" переменную count и хранит её в своей области видимости. Даже после того, как createCounter завершилась, count осталась жива.

4.2 Как это работает под капотом?

Каждая функция в JavaScript при создании получает скрытое свойство [[Environment]], которое ссылается на лексическое окружение, где функция была создана. Когда функция вызывается, создаётся новое окружение, а внешним окружением для него становится то, что сохранено в [[Environment]].

javascript

// Псевдокод того, что происходит
function createCounter() {
// Создаётся окружение Env1 с переменной count
let count = 0;

// Функция запоминает Env1
return function() {
// При вызове ищет count в Env1
count++;
return count;
};
}

Часть 5. Классические примеры замыканий

5.1 Счётчик с шагом

javascript

function createStepCounter(step = 1) {
let count = 0;

return {
increment() {
count += step;
return count;
},
decrement() {
count -= step;
return count;
},
reset() {
count = 0;
return count;
},
getValue() {
return count;
}
};
}

const counter = createStepCounter(5);
console.log(counter.increment());
// 5
console.log(counter.increment());
// 10
console.log(counter.decrement());
// 5
console.log(counter.getValue());
// 5

5.2 Приватные переменные (до появления приватных полей)

javascript

function createBankAccount(initialBalance) {
let balance = initialBalance;
// приватная переменная

return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Сумма должна быть положительной");
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error("Недостаточно средств");
},
getBalance() {
return balance;
}
};
}

const account = createBankAccount(1000);
account.deposit(500);
// 1500
account.withdraw(200);
// 1300
console.log(account.balance);
// undefined (приватно!)
console.log(account.getBalance());
// 1300

5.3 Функции-фабрики

javascript

function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));
// 10
console.log(triple(5));
// 15

5.4 Обработчики событий с сохранением состояния

javascript

function createButtonHandler(message) {
let clickCount = 0;

return function() {
clickCount++;
console.log(`${message} (нажато ${clickCount} раз)`);
};
}

const handleClick = createButtonHandler("Кнопка нажата");
// button.addEventListener("click", handleClick);
// Каждый клик будет увеличивать счётчик

Часть 6. Классическая ловушка: замыкание в циклах

Один из самых известных подводных камней JavaScript.

javascript

// Проблема с var
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
// 3, 3, 3 (все три!)
}, 100);
}

// Почему? Все три функции замыкаются на одну и ту же переменную i.
// К моменту вызова setTimeout (через 100мс) цикл уже завершился, i = 3.

Решения:

Решение 1: Использовать let (создаёт новую переменную для каждой итерации)

javascript

for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
// 0, 1, 2
}, 100);
}

Решение 2: IIFE (создаёт новую область видимости для каждой итерации)

javascript

for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
// 0, 1, 2
}, 100);
})(i);
}

Решение 3: Функция-фабрика

javascript

function createLogger(value) {
return function() {
console.log(value);
};
}

for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 100);
}

Часть 7. Замыкания и память: осторожно, утечки!

Замыкания удерживают переменные в памяти. Если неосторожно использовать их, можно создать утечку памяти.

javascript

function createHeavyClosure() {
const hugeArray = new Array(1000000).fill("данные");
// занимает много памяти

return function() {
// Всего лишь возвращает число, но hugeArray всё ещё в замыкании
return 42;
};
}

const closure = createHeavyClosure();
// hugeArray никогда не будет удалён, даже если closure использует только 42!

Решение: Используйте только нужные переменные.

javascript

function createLightClosure() {
const hugeArray = new Array(1000000).fill("данные");
const result = 42;
// берём только то, что нужно

return function() {
return result;
// hugeArray может быть собран сборщиком мусора
};
}

Часть 8. Модульный паттерн (через замыкания)

До появления ES6 модулей замыкания использовались для создания модулей.

javascript

const calculator = (function() {
// Приватные переменные и методы
let result = 0;

function validateNumber(n) {
if (typeof n !== "number") throw new Error("Нужно число");
return n;
}

// Публичное API
return {
add(n) {
result += validateNumber(n);
return this;
},
subtract(n) {
result -= validateNumber(n);
return this;
},
multiply(n) {
result *= validateNumber(n);
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
}
};
})();

calculator.add(5).multiply(2).subtract(3);
console.log(calculator.getResult());
// 7
console.log(calculator.result);
// undefined (приватно)

Часть 9. var, let, const: битва за область видимости

-2

javascript

// var - функциональная область
if (true) {
var x = 10;
}
console.log(x);
// 10 (вылезло из блока!)

// let/const - блочная область
if (true) {
let y = 20;
const z = 30;
}
// console.log(y); // Ошибка!
// console.log(z); // Ошибка!

// Temporal Dead Zone (TDZ)
// console.log(tdz); // ReferenceError (TDZ)
let tdz = "теперь можно";

Часть 10. Реальные кейсы из продакшена

10.1 Дебаунс (Debounce) - ограничение частоты вызовов

javascript

function debounce(fn, delay) {
let timeoutId = null;
// переменная живёт в замыкании

return function(...args) {
// Очищаем предыдущий таймер
if (timeoutId) clearTimeout(timeoutId);

// Устанавливаем новый
timeoutId = setTimeout(() => {
fn.apply(this, args);
timeoutId = null;
}, delay);
};
}

const debouncedSearch = debounce((query) => {
console.log(`Поиск: ${query}`);
}, 300);

// При быстром вводе вызовется только последний
debouncedSearch("a");
debouncedSearch("ap");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple");
// только этот выполнится через 300мс

10.2 Троттлинг (Throttle) - не чаще раза в интервал

javascript

function throttle(fn, limit) {
let inThrottle = false;
let lastArgs = null;
let lastThis = null;

function execute() {
if (lastArgs) {
fn.apply(lastThis, lastArgs);
lastArgs = null;
lastThis = null;
setTimeout(execute, limit);
} else {
inThrottle = false;
}
}

return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(execute, limit);
} else {
lastArgs = args;
lastThis = this;
}
};
}

const throttledScroll = throttle(() => {
console.log("Скролл!", Date.now());
}, 1000);

window.addEventListener("scroll", throttledScroll);

10.3 Один раз (once)

javascript

function once(fn) {
let executed = false;
let result;

return function(...args) {
if (!executed) {
executed = true;
result = fn.apply(this, args);
}
return result;
};
}

const initialize = once(() => {
console.log("Инициализация...");
return { status: "ready" };
});

console.log(initialize());
// "Инициализация..." → { status: "ready" }
console.log(initialize());
// { status: "ready" } (без повторной инициализации)

Часть 11. Подводные камни

Камень #1: Потеря this в замыкании

javascript

const obj = {
name: "Анна",
createLogger() {
return function() {
console.log(this.name);
// this = global (или undefined)
};
}
};

const logger = obj.createLogger();
logger();
// undefined

// Решение: стрелочная функция или bind
const obj2 = {
name: "Анна",
createLogger() {
return () => {
console.log(this.name);
// this = obj2
};
}
};

Камень #2: Создание замыкания в цикле с асинхронностью (уже разобрали)

Камень #3: Неожиданное поведение с var

javascript

var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0]();
// 3
funcs[1]();
// 3
funcs[2]();
// 3

Итог: Манифест областей видимости и замыканий

  1. Глобальная область - доступна везде. Не злоупотребляйте.
  2. Функциональная область - у var. Переменная видна во всей функции.
  3. Блочная область - у let и const. Переменная видна только в блоке { }.
  4. Цепочка областей - JS ищет переменную от внутренней области к внешней.
  5. Замыкание - функция "запоминает" переменные из внешней области, даже после её завершения.
  6. IIFE - старый способ создания замыканий (сейчас используется реже).
  7. Утечки памяти - замыкания удерживают переменные, будьте осторожны.
  8. let в циклах - решает проблему с асинхронностью (создаёт новую переменную для каждой итерации).

Финальный тест (что выведет?):

javascript

let x = 10;

function outer() {
let x = 20;

function inner() {
console.log(x);
}

return inner;
}

const fn = outer();
fn();

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

Ответы:

  • 20 (замыкание захватило x из outer, а не глобальный)
  • 3, 3, 3 (var - одна переменная на весь цикл)
  • 0, 1, 2 (let - новая переменная для каждой итерации)

Области видимости и замыкания - это фундамент JavaScript. Понимание того, где живут переменные и как функции захватывают окружение, позволяет писать предсказуемый, эффективный и красивый код. Это знание превращает магию в инженерию, а случайности - в осознанные решения. Используйте его мудро.