← Назад к вопросам

Что такое лексическое окружение?

2.0 Middle🔥 191 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Лексическое окружение (Lexical Environment)

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

Что такое лексическое окружение

Лексическое окружение — это внутренняя структура JavaScript, которая хранит переменные и их значения. Каждая функция и блок кода имеют своё лексическое окружение.

Это определяется в момент написания кода (лексически), а не в момент выполнения. Другими словами, внутренняя функция "помнит" переменные из той области, где она была определена.

const global = "глобальное значение";

function outer() {
  const outerVar = "переменная из outer";
  
  function inner() {
    const innerVar = "переменная из inner";
    
    // inner может видеть:
    // 1. innerVar (свое лексическое окружение)
    // 2. outerVar (лексическое окружение функции outer)
    // 3. global (глобальное лексическое окружение)
    console.log(innerVar, outerVar, global);
  }
  
  inner();
}

outer(); // переменная из inner, переменная из outer, глобальное значение

Как это работает внутри

Когда функция выполняется, JavaScript создаёт цепочку лексических окружений:

// Структура лексического окружения примерно такая:
// GlobalLexicalEnvironment {
//   global: "значение",
//   outer: [Function],
//   outerLexicalEnvironment: null
// }
//   ↑
// OuterLexicalEnvironment {
//   outerVar: "переменная из outer",
//   inner: [Function],
//   parentEnvironment: GlobalLexicalEnvironment
// }
//   ↑
// InnerLexicalEnvironment {
//   innerVar: "переменная из inner",
//   parentEnvironment: OuterLexicalEnvironment
// }

Когда JavaScript ищет переменную, он идёт вверх по этой цепочке (scope chain):

function outer() {
  let x = 1;
  
  function middle() {
    let y = 2;
    
    function inner() {
      console.log(x); // ищет x: innerEnv → middleEnv → outerEnv ✅ находит
      console.log(y); // ищет y: innerEnv → middleEnv ✅ находит
      console.log(z); // ищет z: innerEnv → middleEnv → outerEnv → global ❌ не находит → ReferenceError
    }
    
    inner();
  }
  
  middle();
}

outer();

Замыкания и лексическое окружение

Замыкание — это функция вместе со своим лексическим окружением. Это основа функций, которые "запоминают" переменные:

function createCounter(start = 0) {
  let count = start; // лексическое окружение createCounter
  
  // increment — это замыкание, которое "помнит" count
  return function increment() {
    return ++count;
  };
}

const counter1 = createCounter(0);
const counter2 = createCounter(100);

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 101
console.log(counter1()); // 3

// У каждого замыкания своё лексическое окружение
// counter1 помнит свой count
// counter2 помнит свой count

Практические примеры

1. Фабрика функций

function makeAdder(x) {
  // лексическое окружение функции makeAdder содержит x
  return function(y) {
    // замыкание помнит x из родительского окружения
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(3));  // 8
console.log(add10(3)); // 13

// Каждое замыкание имеет свой x в своём лексическом окружении

2. Проблема с циклом

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

// ❌ Проблема: все замыкания помнят одну переменную i
var functions = [];
for (var i = 0; i < 3; i++) {
  functions.push(function() {
    return i; // все помнят одно и то же i
  });
}

console.log(functions[0]()); // 3
console.log(functions[1]()); // 3
console.log(functions[2]()); // 3
// После цикла i = 3, и все замыкания возвращают 3

// ✅ Решение 1: IIFE (Immediately Invoked Function Expression)
var functions = [];
for (var i = 0; i < 3; i++) {
  functions.push((function(j) {
    return function() {
      return j; // каждое замыкание помнит свой j
    };
  })(i));
}

console.log(functions[0]()); // 0
console.log(functions[1]()); // 1
console.log(functions[2]()); // 2

// ✅ Решение 2: let (проще и современнее)
var functions = [];
for (let i = 0; i < 3; i++) { // let создаёт новое лексическое окружение для каждой итерации
  functions.push(function() {
    return i;
  });
}

console.log(functions[0]()); // 0
console.log(functions[1]()); // 1
console.log(functions[2]()); // 2

Переменные в лексическом окружении

Важный момент: в лексическом окружении хранятся не значения, а ссылки на переменные:

function outer() {
  let x = 10;
  
  function inner() {
    console.log(x); // ссылка на переменную x
  }
  
  x = 20; // изменяем значение переменной x
  inner(); // 20 — видит обновленное значение
  
  return inner;
}

const inner = outer();
inner(); // 20

React и лексическое окружение

Замыкания и лексическое окружение — основа React hooks:

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  // count и setCount находятся в лексическом окружении этой функции
  
  const increment = useCallback(() => {
    // increment — замыкание, которое помнит setCount
    setCount(prev => prev + 1);
  }, []);
  
  return { count, increment };
}

function Counter() {
  const { count, increment } = useCounter(0);
  // count — из лексического окружения useCounter
  
  const handleClick = () => {
    // handleClick помнит increment
    increment();
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

EventListeners и лексическое окружение

function setupListeners() {
  for (let i = 0; i < 3; i++) {
    const button = document.getElementById(`btn-${i}`);
    
    // каждый слушатель — замыкание, помнящее свой i
    button.addEventListener("click", function() {
      console.log(`Кнопка ${i} была нажата`); // каждое помнит свой i
    });
  }
}

setupListeners();

Утечки памяти через лексическое окружение

// ❌ Потенциальная утечка памяти
function createHeavyClosures() {
  const hugeArray = new Array(1000000).fill("data");
  
  return [
    function() { console.log(hugeArray[0]); }, // помнит весь массив
    function() { console.log(hugeArray[1]); }, // помнит весь массив
    function() { console.log(hugeArray[2]); }, // помнит весь массив
  ];
}

const closures = createHeavyClosures();
// hugeArray не может быть собран сборщиком мусора
// пока существует хотя бы одно из замыканий

// ✅ Правильно: сохраняй только нужные значения
function createEfficientClosures() {
  const data = ["a", "b", "c"];
  
  return data.map((item, index) => {
    // каждое замыкание помнит только item, не весь массив
    return function() { console.log(item); };
  });
}

Отладка лексического окружения

// DevTools показывает лексическое окружение функции
function debugExample() {
  const x = 10;
  const y = 20;
  
  function inner() {
    debugger; // Open DevTools → Scope → Local, Closure
    console.log(x, y);
  }
  
  inner();
}

Заключение

Лексическое окружение — это основа того, как JavaScript управляет переменными и областью видимости. Ключевые моменты:

  1. Определяется лексически — в момент написания кода
  2. Создаёт замыкания — функции помнят переменные из родительских окружений
  3. Область видимости — основа работы scope chain
  4. Влияет на производительность — неправильное использование может привести к утечкам памяти

Понимание лексического окружения критично для написания эффективного и предсказуемого JavaScript кода.