Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

Понимание работы функций в немалой степени зависит от знания некоторых деталей реализации языка. К таким деталям относятся окружения (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.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →