Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос нашим менторам. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

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

Понимание работы функций в немалой степени зависит от знания некоторых деталей реализации языка. К таким деталям относятся окружения (Environment). Разговор о них шел в соответствующем уроке «Введения в программирование». Советую освежить память, пересмотрев тот урок или перечитав конспект. После этого урока вы будете еще лучше разбираться в окружениях.

Познакомимся с термином «словарь». Словарь — это набор пар «ключ - значение». Зная ключ, можно получить значение. Точно так же, как и при работе с обычным бумажным словарем, где ключ — это слово, а значение — определение слова. В данном уроке важно только концептуальное понимание, без конкретных реализаций.

Каждый раз, когда в программе вызывается функция, внутри интерпретатора создается специальный словарь LexicalEnvironment (лексическое окружение), привязанный к этому вызову. Все определения констант, переменных и прочего внутри функции автоматически записываются в словарь. Имя определения (идентификатор, то есть имя константы, переменной и так далее) становится ключом, а значение определения становится значением в словаре. К таким определениям относятся аргументы, константы, функции, переменные и т.д. Лексическое окружение — это хранилище для данных в памяти и механизм для извлечения этих данных при обращении.

В примере ниже в комментариях показано состояние словаря перед выполнением каждой строчки кода. Не забывайте, что наполнение словаря происходит при вызове функции, а не при определении.

const showWarning = (field) => {
  // LexicalEnvironment = { field: 'email' }
  const warning = `verify your ${field}, please`;
  // LexicalEnvironment = { warning: 'verify your email, please', field: 'email' }
  console.log(warning);
}

showWarning('email'); // => verify your email, please

Код console.log(warning) активизирует поиск значения идентификатора warning в лексическом окружении.

В процессе выполнения функции значения переменных могут меняться, что сразу же отражается в лексическом окружении. После выполнения функции её лексическое окружение уничтожается, а занятая им память освобождается.

Из этого поведения есть исключение — возврат функции. В следующем уроке мы рассмотрим связанный с ним механизм так называемых «замыканий». Ранее мы разбирали его в «Введении в программирование».

Окружение есть не только у функций. Любой идентификатор, определенный на уровне модуля, попадает в лексическое окружение модуля. Кроме того, существует и глобальное окружение. Благодаря ему мы с легкостью используем в JS такие функции, как console.log или Math.sqrt, даже особо не задумываясь, откуда они берутся.


const number = 5;
const square = () => number ** 2;
square(); // 25

Такой код работает — и это для нас не секрет, но как он вяжется с механизмом окружений? А вот как: интерпретатор производит поиск значения идентификатора не только в локальном лексическом окружении (в том, где используется идентификатор), но и во внешнем окружении. Поиск начинается с локального окружения, и если в нём не найден нужный идентификатор, то просмотр идет дальше, вплоть до уровня модуля, а затем и до глобального уровня.

Внешним окружением по отношению к функции считается окружение, в котором функция была объявлена (а не вызвана!). Если разбить пример выше на два файла, то разница станет очевидной.

Так сработает:

// module1.js
const number = 5;
export const square = () => number ** 2;

// module2.js
import { square } from './module1';
square(); // 25

А так нет:

// module1.js
export const square = () => number ** 2;

// module2.js
import { square } from './module1';

const number = 5;
square(); // ReferenceError: number is not defined

Если подумать логически, так и должно быть. Представьте: если бы сработал второй вариант, то автоматически это бы означало, что вы можете случайно создать константу с именем, совпадающим с именем константы внутри функции, написанной другим человеком. Как при этом будет работать код — предположить невозможно.

Попробуйте самостоятельно ответить на вопрос: сработает ли такой код, в котором константа определена позже её использования внутри функции?

const square = () => number ** 2;
const number = 5;

square(); // 25

Ответ: сработает.

Окружение — это не «всё, что было объявлено до функции, в которой я использую эти объявления». Не важно, что number появился позже использования внутри функции. Главное, что вызов функции square происходит позже определения number, а значит к этому времени идентификатор уже был добавлен в окружение, внутри которого была создана функция square.

Переменные

Когда мы работаем с константами, всё просто. Нет изменений — нет проблем. В случае с переменными ситуация становится сложнее.

const square = () => number ** 2;

let number = 5;
square(); // 25

number = 3;
square(); // 9

Изменение переменной следует читать как «изменение значения ключа в окружении». Соответственно, обращение к number всегда вернет последнее присвоенное значение. Завязка на переменные, описанная в коде выше, должна восприниматься как абсолютное зло. Она порождает неявные зависимости, сложный код и отладку. Функция автоматически перестает быть чистой, так как начинает зависеть от внешнего контекста.

Вложенные функции

Напомню вам слегка модифицированный код курса «Введение в программирование»:

const factorial = (n) => {
  const iter = (counter, acc) => {
    if (counter > n) {
      return acc;
    }
    return iter(counter + 1, counter * acc);
  };

  return iter(1, 1);
};

factorial(5); // 120

В этом коде реализовано вычисление факториала с применением итеративного процесса. Внутри функции factorial определяется внутренняя функция iter, которая накапливает аккумулятор, вызываясь рекурсивно. Условие выхода из рекурсии — попытка посчитать число большее, чем нужно.

В этой проверке используется переменная n, которая явно в iter не передавалась. Но благодаря тому, как работают окружения, любые функции (в том числе и вложенные), определенные внутри factorial, имеют к ней доступ. Как видно из кода, n используется как константа, а значит такое использование абсолютно безопасно.

Перекрытие (Shadowing)

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

const f = (coll) => {
  const iter = (item, coll) => {
    // using coll
  }
  // ...
}

Несмотря на то, что сам код остается рабочим, перекрытие больше не позволяет обратиться к идентификатору из внешнего окружения, ведь поиск всегда происходит сначала в локальном окружении, а уже затем во внешних. Но еще большей проблемой является то, что такой код сложнее в анализе. Глядя на него недостаточно видеть имена, нужно также учитывать их контекст, так как одно и то же имя на разных строках может означать разные вещи. Если запустить линтер для подобного кода, то он укажет на перекрытие как на плохую практику программирования. Подробнее об этом можно прочитать в правилах Eslint.


<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят менторы из команды Хекслета или другие студенты.

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг». Защита от спама reCAPTCHA «Конфиденциальность» и «Условия использования».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг». Защита от спама reCAPTCHA «Конфиденциальность» и «Условия использования».