Как это работает
Лексическое окружение (Lexical Environment)
При каждом вызове функции движок создаёт объект Lexical Environment с двумя частями:
- Environment Record — все локальные переменные и параметры
- [[OuterEnv]] — ссылка на окружение, в котором функция была создана
Поиск переменной идёт по цепочке: текущее окружение → [[OuterEnv]] → его [[OuterEnv]] → … → глобальное. Не нашли нигде — ReferenceError.
function outer() {
let x = 10 // Environment Record: { x: 10 }
function inner() {
// [[OuterEnv]] → окружение outer
console.log(x) // нет локально → ищет в [[OuterEnv]] → 10
}
return inner
}
Замыкание работает именно благодаря [[OuterEnv]] — внутренняя функция хранит указатель на окружение, в котором создана.
Scope chain по шагам
const greeting = 'Привет'
function createGreeter(name) {
const prefix = '>>> '
return function greet() {
console.log(prefix + greeting + ', ' + name)
}
}
const fn = createGreeter('Мир')
fn() // ">>> Привет, Мир"
- Создаётся глобальное окружение:
{ greeting, createGreeter }.[[OuterEnv]]=null - Вызов
createGreeter('Мир')→ новое окружение:{ name: 'Мир', prefix: '>>> ' }.[[OuterEnv]]→ глобальное - Создаётся
greet→ запоминает окружениеcreateGreeterв[[Environment]] createGreeterзавершается, но её окружение не уничтожается — на него ссылаетсяgreet- Вызов
fn()→greetищетprefixв окруженииcreateGreeter,greetingв глобальном
Цепочка greet → createGreeter → global — это scope chain.
var vs let в циклах
var создаёт одну переменную на всю функцию. let — новую на каждую итерацию:
// var: одна переменная i на все итерации
for (var i = 0; i < 3; i++) { /* один i */ }
console.log(i) // 3 — i доступна за пределами цикла
// let: новая привязка i на каждую итерацию
for (let i = 0; i < 3; i++) { /* свой i */ }
// console.log(i) — ReferenceError
Все замыкания в цикле с var ссылаются на одну переменную → видят финальное значение. С let каждое замыкание получает свою копию. Подробный разбор — в частых проблемах.
Вложенные замыкания
Цепочка окружений может быть произвольной глубины:
function a() {
let x = 1
function b() {
let y = 2
function c() {
console.log(x + y + 3) // c → b → a
}
return c
}
return b
}
a()()() // 6
Когда окружение уничтожается
Пока есть хотя бы одна ссылка — окружение живёт. Нет ссылок — GC собирает:
function create() {
const bigData = new Array(1_000_000)
return () => bigData.length
}
let fn = create() // окружение живёт
fn = null // ссылок нет → GC собирает окружение и bigData
V8 ещё на этапе парсинга анализирует, какие переменные внешней функции реально нужны внутренней. Только эти переменные попадают в объект Context, который живёт в куче. Всё остальное остаётся на стеке и освобождается при выходе из функции. Поэтому замыкание захватывает ровно столько, сколько использует — без лишнего расхода памяти.
Подробнее о context allocation и оптимизациях V8 — в главе Движок V8: Context Allocation.
Итого
| Концепция | Описание |
|---|---|
| Lexical Environment | Переменные (Environment Record) + ссылка на внешнее окружение ([[OuterEnv]]) |
| Scope chain | Цепочка окружений от текущего к глобальному |
| var vs let | var — одна привязка на функцию, let — новая на каждый блок/итерацию |
| Время жизни | Пока есть ссылка → живёт. Нет ссылок → GC собирает |