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

ВАЖНО

Изначально в начале конспекта и урока речь шла о глобальном и локальном окружении, но объяснялась фактически глобальная и локальная область видимости. В видео эта тема до сих пор называется «глобальное и локальное окружение».

Под лексической областью видимости можно понимать просто механизм поиска значений: смотрим в текущей области, если нет — идём на уровень выше, и так далее. Слово «лексический» означает, что видимость задаётся исключительно текстом программы, исходным кодом. То есть можно смотреть на программу, не запуская её, и понять область видимости в любой точке.

В других языках может быть не лексический механизм, а динамический (dynamic scope).

Область видимости — это широкое понятие, означающее грубо говоря «интерпретатор в разных местах кода видит разные штуки». Лексическая область видимости — это конкретный механизм, одно из правил для области видимости. Этот механизм применяется в JavaScript и большинстве других языков.

Путаница возникает ещё потому, что в разделе про Область видимости мы смотрим на примеры, которые работают по правилу «лексическая область видимости». От этого никуда не деться, так как язык JavaScript работает именно так.

Конспект урока

  • Область видимости (scope) компонентов — это местоположение, где эти компоненты доступны.
  • Компоненты, созданные снаружи функций, инструкций с if, циклов и так далее, находятся в глобальной области видимости
  • Фигурные скобки { } задают новую локальную область видимости

Глобальная против локальной

Локальные константы и переменные не видимы снаружи их области видимости:

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // ReferenceError: x is not defined

Но если x представлен глобально, то он доступен:

const x = 55;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // 55

Возможен доступ к внешней области видимости:

let a = 0;

const changer = () => {
  a++;
}

console.log(a);   // 0
changer();
console.log(a);   // 1

Функция фактически производит что-то, только когда она вызывается, а не задаётся, поэтому изначально a это 0, но после того, как вызвана changer, a становится 1.

Лексическая область видимости

JavaScript пытается найти значение в текущем окружении. Но значение не находится и JavaScript выходит наружу, на один уровень за попытку, пока не найдёт значение или не поймет, что значение невозможно найти.

let a = 7;
let b = 10;

const multiplier = () => {
  let a = 5;
  return a * b;
}

multiplier(); // 50

Здесь, в выражении a * b , функция multiplier использует локальную a (потому что она обнаружена локально), и наружную b (потому что локально b найдена не была).

Замыкания

const createPrinter = () => {
  const name = "King";

  const printName = () => {
    console.log(name);
  }

  return printName;
}

const myPrinter = createPrinter();
myPrinter();    // King

myPrinter — это функция, которая была возвращена createPrinter. Несмотря на то, что вызов createPrinter окончен и константы name больше не существует, значение запомнено в myPrinter.

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

Optional reading


Транскрипт урока

Часть I. Окружение

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

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

const age = 29;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

let result = true;

Константа age , функция multiplier и переменная result — все во внешнем окружении. Эти компоненты имеют "глобальную область видимости". Область видимости означает "область, где компоненты доступны".

Внутри функции multiplier есть константа x. Поскольку она внутри блока кода, это локальная константа, а не глобальная. Она видна только внутри этой функции, но не снаружи. У неё локальная область видимости.

В функции multiplier есть ещё один компонент из локальной области видимости — аргумент num. Он не задан так же чётко, как константы или переменные, но ведёт себя почти как локальная переменная.

У нас нет доступа к x снаружи, как будто её там не существует:

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // ReferenceError: x is not defined

console.log вызывается в глобальном окружении, а x не задан в этом глобальном окружении. Поэтому мы получаем Reference Error.

Мы можем задать x глобально:

const x = 55;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // 55

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

Любой блок кода окружёный фигурными скобками превращается в локальное окружение. Вот пример с блоком if:

let a = 0;

if (a === 0) {
  const local = 10;
}

console.log(local);

То же работает для циклов while и for.

Ок, локальное не видимо снаружи. Но глобальное видимо везде. Даже внутри чего-то? Да!

let a = 0;

const changer = () => {
  a++;
}

console.log(a);   // 0
changer();
console.log(a);   // 1

Эта глобальная переменная a изменилась внутри функции changer. Функция вообще выполняет что-то только когда её вызывают, а не когда её определяют, так что вначале a это 0, но после того как вызывается changer, a становится 1.

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

Часть II. Лексическая область видимости

Взгляните на эту программу:

let a = 7;
let b = 10;

const multiplier = () => {
  let a = 5;
  return a * b;
}

multiplier(); // 50

Функция multiplier возвращает произведение a и b. a задано внутри, а b — нет.

Пытаясь решить умножение a * b, JavaScript ищет значения a и b. Он начинает искать локально и выходит наружу, по одной области видимости за шаг, пока он не найдёт то, что ему нужно или пока не поймёт, что это невозможно найти.

Поэтому в данном примере JavaScript начинает с поиска a внутри локальной области видимости — внутри функции multiplier. Он находит значение сразу и переходит к b. Невозможно найти значение b в локальной области видимости, поэтому он переходит к наружной области. Тут он находит b — это 10. a * b превращается в 5 * 10, а затем в 50.

Весь этот кусок кода мог бы быть внутри другой функции, и ещё внутри другой функции. И если бы b не нашлась здесь, JavaScript продолжил бы искать b за пределами функции, слой за слоем.

Заметьте, что a = 7 не затронула вычисления, a была найдена внутри, поэтому внешняя a не сыграла роли.

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

Часть III. Замыкания

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

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

const f = () => {
  return 0;
}

f — довольно бесполезная функция, она всегда возвращает 0. Весь этот набор состоит из двух частей: константы и самой функции.

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

Мы использовали аналогию в предыдущих уроках: константы как листы бумаги — имя на одной стороне, значение на другой. Следовательно, f — лист бумаги с f на одной стороне и описанием запускаемой функции на другой.

Когда вы вызываете эту функцию, вот так:

f();

… создаётся новый ящик, основываясь на описании на этом листе бумаги.

Ок, вернёмся к замыканиям. Рассмотрим следующий код:

const createPrinter = () => {
  const name = "King";

  const printName = () => {
    console.log(name);
  }

  return printName;
}

const myPrinter = createPrinter();
myPrinter();    // King

Функция createPrinter создаёт константу name и затем функцию с именем printName. Обе они локальные для функции createPrinter, и доступны только внутри createPrinter.

У самой printName нет локальных компонентов, но у неё есть доступ к области видимости, где она сама находится, внешней области, где задана константа name.

Затем функция createPrinter возвращает функцию printName. Помните, определения функций — это описания запущенных функций, просто фрагменты информации, как числа или строки. Поэтому мы можем вернуть определение функции, как мы возвращаем число.

Во внешней области видимости мы создаём константу myPrinter и задаём ей значение, которое возвращает createPrinter. Он возвращает функцию, так что теперь myPrinter это функция. Вызовите ее, и на экран выведется "King".

Тут есть странная штука: эта константа name была создана внутри функции createPrinter. Функция была вызвана и исполнена. Как мы знаем, когда функция заканчивает работу, она больше не существует. Этот магический ящик исчезает со всеми своими внутренностями.

НО он возвратил другую функцию, и уже она как-то запомнила константу name. Поэтому когда мы вызывали myPrinter, она вывела "King" — запомненное значение, при том, что область видимости, где оно было задано больше не существует.

Функция, которая была возвращена из createPrinter, называется замыканием. Замыкание — это сочетание функции и окружения, где она была задана. Функция "замкнула" в себе некоторую информацию из области видимости.

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

Вы заметите, как часто эти идеи используются в программировании, и мы рассмотрим их мощь в следующих курсах.

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

Хекслет

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