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

Есть две преобладающих модели того, как работает/устроена область видимости: лексическая (используется в большинстве языков программирования) и динамическая. В этой части курса будут рассмотрены обе эти модели, но основное внимание будет уделяться лексической области видимости, так как именно она используется в JavaScript.

Лексическая (или статическая)

В части курса — Устройство JavaScript-движков, говорилось о том, что сначала исходный код программы последовательно анализируется и разбирается компилятором. Первым традиционным этапом работы стандартного компилятора является лексический анализ исходного кода.На этом этапе каждая строка кода посимвольно анализируется и разбивается на значимые для языка части. Эти имеющие смысл для языка части называются лексемами.

Например, представьте программу:

var a = 2;

Эта программа, вполне вероятно, будет разбита на следующие лексемы:

var  объявление переменной,
a  идентификатор (имя) переменной,
=  оператор присваивания,
2  число,
;  конец инструкции

Пробел может быть сохранен или не сохранен как лексема в зависимости от того имеет он смысл или нет.

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

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

Пример:

let value = 2;

function showValue() {
 console.log("Value from showValue: " + value); // 2
}

function wrapper() {
 let value = 3;
 console.log("Value from wrapper: " + value); // 3

 showValue();
}
wrapper();

При выполнении этого примера сначала вызывается функция wrapper, которая сначала выведет значение переменной value и затем вызовет функцию showValue. В области видимости функции wrapper переменная value “затеняет” переменную, объявленную в глобальной области видимости и поэтому функция wrapper выведет — "Value from wrapper: 3". Далее вызовется функция showValue, которая тоже отображает значение переменной value, только в этой функции оно является числом 2.

Так как лексические области видимости (связи переменных с их идентификаторами) формируются на этапе лексического разбора, а не на этапе выполнения программы, то переменная value в функции showValue будет взята из родительской (для showValue) области видимости, где её значение — 2.

То есть сначала, еще до выполнения самой программы, произойдет следующее:

  1. Лексический анализатор дойдет до строки функции showValue:

    console.log("Value from showValue: " + value);
    

    в которой запрашивается переменная по идентификатору value

  2. Лексический анализатор начнет поиск этой переменной, начиная с текущей области видимости, где эта переменная запрашивается (в функции showValue).
  3. Не найдя переменную в локальной области видимости, лексический анализатор обратится к окружению функции showValue и найдет её в родительской области видимости, где такая переменная объявлена.
  4. Лексический анализатор установит связь между этой переменной из родительской области видимости и идентификатором, который запрашивается в showValue

Поэтому несмотря на то, что showValue вызывается из функции wrapper, где объявлена своя переменная value, функция showValue выведет значение той переменной, которая была зафиксирована во время лексического анализа, то есть число 2.

Динамическая

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

Если бы в JavaScript была динамическая область видимости, то при выполнении каждой функции из предыдущего примера в консоли выводилось бы цифра 3

let value = 2;

function showValue() {
 console.log("Value from showValue: " + value); // 3
}

function wrapper() {
 let value = 3;
 console.log("Value from wrapper: " + value); // 3

 showValue();
}
wrapper();

Результат выполнения был бы таким:

> "Value from wrapper: 3"
> "Value from showValue: 3"

Функция wrapper будет выводить 3, так как переменная value определена в текущей области видимости, где ей присвоено значение 3.

Теперь разберем почему при динамической области видимости в функции showValue значение переменной value тоже будет 3. В этом случае, не найдя переменную value в локальной области видимости функции showValue, лексический анализатор вместо поднятия по цепочке окружающих лексических областей видимости, будет взбираться вверх по стеку вызовов функций, чтобы найти откуда showValue() была вызвана. Поскольку showValue() вызывалась из wrapper(), он будет искать переменную именно там — в области видимости wrapper(), и обнаружив её, установит связь между этой переменной из области видимости wrapper() и идентификатором, который запрашивается в showValue.

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

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

Лексическое окружение

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

Переменные начинают своё существование тогда, когда выполнение программы достигает их области видимости. И тогда этим переменным необходимо место для хранения, чтобы впоследствии к ним можно было обратиться даже из вложенных областей видимости. В спецификации JavaScript, структура данных, которая обеспечивает такое пространство для хранения переменных в памяти, а также предоставляет механизм их поиска и возможность получить к ним доступа из внутренних областей, называется лексическим окружением. Оно сопоставляет идентификаторы (имена) с переменными и функциями уже не только в рамках одной области видимости, а для целых цепочек вложенных друг в друга областей. Его структура очень похожа на структуру объектов JavaScript.

Как и в предыдущей части есть две модели взаимодействия и работы с переменными: Лексическое окружение (связанное с цепочкой областей видимости) и Динамическое (связанное со стеком контекстов выполнения). О Динамической модели и контекстах выполнения речь пойдет в следующей части. А в этой части будет рассматриваться устройство Лексического окружения.

Как говорилось в предыдущей части — суть лексических областей видимости в том, что даже на этапе выполнения программы, они сохраняют связь со своими внешними/родительскими областями, которые были определены на этапе лексического разбора (то есть сформированы исходя из того, где переменные, функции и инструкций изначально были написаны в коде). За счет сохранения такой связи формируется целая цепочка областей видимости, каждая из которых знает свою родительскую область видимости. Каким образом это реализуется в JavaScript? Каждая функция, которая является в свою очередь отдельной областью видимости, на этапе инициализации, запоминает свою родительскую область видимости, где она содержится. Это происходит за счет того, что у каждой функции есть внутреннее свойство [[Environment]] которое сохраняет в себе ссылку на внешнюю область видимости (это свойство недоступно нам из самой программы и используется JavaScript-движком).

Рассмотрим следующий пример,

var x = 10;

function foo() {
 var y = 20;
}
function bar() {
 var z = 30;
}

Схематично области видимости и значения свойства [[Environment]] можно показать так:

// global scope

var x = 10;

function foo() {
 // foo scope
 // [[Environment]] = global scope

 var y = 20;
}

function bar() {
 // bar scope
 // [[Environment]] = global scope

 var z = 30;
}

Если рассматривать внутреннее устройство Лексического окружения в рамках спецификации ES9. То оно состоит из Записи Окружения и ссылки на внешнее лексическое окружение.

Обычно Лексическое Окружение ассоциируется с определёнными синтаксическими структурами кода JavaScript, такими как объявление функций или блоками инструкций. Каждый раз когда обрабатывается такой код, например вызывается функция, то для этой новой области видимости создаётся своё Лексическое окружение. Для этого окружения формируется:

  1. Запись Окружения environment record, которая содержит в себе связи идентификаторов с переменными, которые созданы в области видимости этого Лексического окружения. Также она содержит и другую необходимую информацию, например значение ключевого слова this, о котором будет рассказано в другой части.
  2. Ссылка outer, которая указывает на внешнее/родительское окружение для этой области видимости. Именно в это поле попадает значение внутреннего свойства [[Environment]], которое хранит в себе ссылку на родительскую область видимости. И поэтому всегда существует цепочка Лексических окружений. Она начинается с текущего (выполняющегося в данный момент) Лексического окружения, продолжается внешними окружениями, и заканчивается глобальным Лексическим окружением, у которого поле outer равно null.

Сразу стоит сказать, что Лексические окружения является исключительно описательным концепцией устройства и работы программы на JavaScript. Оно не имеет какой-либо реализации в коде JavaScript и в программе нет прямого доступа к нему и возможности манипулировать им напрямую. Но понимание этой концепции дает понимание о структуре кода, значениях интересующих нас переменных и доступности тех или иных данных в определенной точке программы.

Теперь рассмотрим как будет выглядеть устройство Лексических окружений для предыдущего примера, после выполнения функций в конце программы:

// globalEnvironment
// environmentRecord = {x: 10, foo: function foo(){...}, bar: function bar(){...}}
// outer: null — нет родительского окружения
var x = 10;

function foo() {
 // fooEnvironment
 // environmentRecord = {y: 20}
 // outer: globalEnvironment — глобальное окружение

 var y = 20;
}

function bar() {
 // barEnvironment
 // environmentRecord = {z: 30}
 // outer: globalEnvironment — глобальное окружение

 var z = 30;
}
foo();
bar();
// <--- Лексические окружения рассматриваются из этой точки, то есть после выполнения всех функций

Или можно представить Лексические окружения в виде объектов:

// глобальное Лексическое окружение
globalEnvironment = {
 environmentRecord: {
  // внутренние свойства глобального окружения...

  // наши привязки переменных:
  x: 10,

  //и объявленные функции
  foo: function foo() {
   /*...тело функции...*/
  },
  bar: function bar() {
   /*...тело функции...*/
  }
 },

 outer: null // нет родительского окружения
};

// Лексическое окружение функции foo
fooEnvironment = {
 environmentRecord: {
  y: 20
 },
 outer: globalEnvironment
};

// Лексическое окружение функции bar
barEnvironment = {
 environmentRecord: {
  z: 30
 },
 outer: globalEnvironment
};

Схематично это можно изобразить так:

схема лексических окружений

Теперь разберем пример с большей вложенностью функций.

var x = 10;

function foo() {
 var y = 20;

 function bar() {
  var z = y + x;
 }
 bar();
}
foo();

Пошаговый этап выполнения программы от начала до конца будет рассматриваться в теме контекстов выполнения. А сейчас пока рассмотрим сформированные Лексические окружения уже после выполнения функций.

// globalEnvironment
// outer: null — нет родительского окружения
// environmentRecord = {x: 10, foo: function foo(){...}}

var x = 10;

function foo() {
 // fooEnvironment
 // outer: globalEnvironment — глобальное окружение
 // environmentRecord = {y: 20, bar: function bar(){...}}

 var y = 20;

 function bar() {
  // barEnvironment
  // outer: fooEnvironment — глобальное окружение
  // environmentRecord = {z: 30}

  var z = y + x; // переменная y найдена в fooEnvironment, а переменная x в globalEnvironment
 }
 bar();
}
foo();
// <--- Лексические окружения рассматриваются из этой точки, то есть после выполнения всех функций

Здесь важно заметить, что на строчке

var z = y + x;

интерпретатор, сначала пытается найти нужную переменную в текущей записи окружения (сначала y, потом x), а затем, не обнаружив её в текущей записи окружения, ищет во внешнем окружении. Переменная y будет найдена в родительском окружении fooEnvironment. А, чтобы найти переменную х, интерпретатор пойдет по цепочке дальше, до самого глобального окружения globalEnvironment.

Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в поле outer, которое в свою очередь устанавливается из внутреннего свойства функции — [[Environment]]. Эти свойства закрыты от прямого доступа, но знание о них очень важно для понимания того, как работает JavaScript. Если в текущем окружении нужной переменной нет, то благодаря существующему в каждом лексическом окружении полю outer, где содержится ссылка на родительское окружение, поиск продолжается до тех пор, пока переменная не обнаружится в каком-то из внешних окружений. В случае, если интерпретатор дошел по цепочке до глобального окружения, у которого поле outer равняется null, и при этом и там не нашел необходимую переменную (то есть переменная не была объявлена нигде в коде), тогда возникнет ошибка ReferenceError: nameOfVariable is not defined.

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

Схематично Лексические окружения в этом примере можно представить так:

схема лексических окружений

Обнаружили ошибку или хотите добавить что-то своё в документацию? Отредактируйте эту страницу на GitHub!

Оставить комментарий