Лексическое окружение (LexicalEnvironment) и Замыкание (Closures) в EcmaScript

Привет, Хабр!

Давно ничего не писал, большая загруженность на проекте крайние несколько недель, но сейчас появилось свободное время, поэтому решил представить вашему вниманию новую статью.

Сегодня мы продолжим разбирать ключевые концепции EcmaScript, поговорим о Лексическом окружении и Замыкании. Понимание концепции Лексического окружения очень важно для понимания замыкания, а замыкание это основа очень многих хороших техник и технологий в мире JS (который основан на спецификации EcmaScript).

Итак, начнём.

Лексическое окружение (LexicalEnvironment, ЛО, LE)
Официальная спецификация ES6 определяет этот термин как:
Lexical Environment — это тип спецификации, используемый для разрешения имён идентификаторов при поиске конкретных переменных и функций на основе лексической структуры вложенности кода ECMAScript. Лексическая окружение (Lexical Environment) состоит из записи среды и, возможно, нулевой ссылки на внешнюю Лексическую среду.
Разберёмся подробнее.

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

Технически ЛО представляет собой объект с двумя свойствами:

  • запись окружения (именно тут хранятся все объявления)
  • ссылка на ЛО порождающего контекста.


Через ссылку на контекст-родитель текущего контекста мы можем в случае необходимости получить ссылку на «контекст-дедушку» контекста-родителя и так далее до глобального контекста, ссылка на родитель которого будет null. Из этого определения следует, что Лексическое окружение — это связь сущности с контекстами её породившими. Своего рода ScopeChain у функций — это аналог Лексического окружения. Подробно о ScopeChain мы говорили в этой статье.

let x = 10;
let y = 20;
const foo = z => {
let x = 100;
return x + y + z;
}

foo(30);//вернёт 150. ЛО для foo будет выглядеть так {record: {z: 30, x: 100}, parent: __parent};
// __parent будет указывать на глобальное ЛО {record: {x: 10, y: 20}, parent: null}

Технически процесс разрешения имён идентификаторов будет происходить также как и в ScopeChain, т.е. будет происходить последовательный опрос объектов в цепи ЛО до тех пор, пока не будет найден нужный идентификатор. Если идентификатор не найден, то ReferenceError.

Лексическое окружение создаётся и наполняется на этапе создания контекста. Когда текущий контекст заканчивает своё выполнение, он удаляется из стека вызовов, но его Лексическое окружение может продолжать жить до тех пор, пока на него есть хоть одна ссылка. Это одно из преимуществ современного подхода к проектированию языков программирования. Думаю об этом стоит рассказать!

Стековая организация vs Динамически распределяемая память
В стековых языках локальные переменные хранятся в стеке, который пополняется при активации функции, при выходе из функции её локальные переменные удаляются из стека.

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

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

function testFn() {
var locaVar = 10; // свободная переменная для функции innerFn
function innerFn(p) {
alert(p + localVar);
}

return innerFn; //возврат функции
}

var test = testFn();// присвоим innerFn в переменную
test();//в стековых языках это бы не работало

При стековой организации не был бы возможен ни поиск locaVar во внешней LexicalEnvironment, ни возврат функции innerFn, т.к. innerFn это тоже локальная декларация для testFn. По завершению testFn все её локальные переменные просто удалились бы из стека.

Поэтому была предложена другая концепция — концепция динамически распределяемой памяти (куча, heep) + сборщик мусора (garbage collector) + подсчёт ссылок. Суть этой концепции проста: пока на объект существует хоть одна ссылка, он не удаляется из памяти. Более подробно можно почитать тут.

Замыкание (Closures)
Замыкание — совокупность блока кода и данных того контекста, в котором тот блок порождён, т.е. это связь сущности с порождающими контекстами через цепь ЛО или SсopeChain.

Позволю себе процитировать очень хорошую статью на эту тему:

function person() {
let name = ‘Peter’;

return function displayName() {
console.log(name);
};
}
let peter = person();
peter(); // prints ‘Peter’

Когда выполняется функция person, JavaScript создаёт новый контекст выполнения и лексическое окружение для функции. После того, как эта функция завершится, она вернёт displayName функцию и назначится на переменную peter.

Таким образом, её лексическое окружение будет выглядеть так:

personLexicalEnvironment = {
environmentRecord: {
name : ‘Peter’,
displayName: < displayName function reference>
}
outer: <globalLexicalEnvironment>
}

Когда функция person завершится, её контекст выполнения выкинется из стека. Но её лексическое окружение всё ещё останется в памяти, так как на него ссылается лексическое окружение его внутренней функции displayName. Таким образом, её переменные всё ещё будут доступны в памяти.

При выполнении функции peter (которая на самом деле является отсылкой к функции displayName), JavaScript создаёт новый контекст выполнения и лексическое окружение для этой функции.

Так что его лексическое окружение будет выглядеть таким образом:

displayNameLexicalEnvironment = {
environmentRecord: {

}
outer: <personLexicalEnvironment>
}

В функции displayName нет переменной, её запись окружения будет пуста. Во время выполнения этой функции, JavaScript будет пытаться найти переменную name в лексическом окружении функции.

Так как там нет переменных в лексическом окружении функции displayName, она будет искать во внешнем лексическом окружении, то есть, лексическом окружении функции person, которое до сих пор в памяти. JavaScript найдёт эту переменную и name выводится в консоль.
Важнейшей характеристикой замыкания в ES является то, что оно использует статическую область видимости (в ряде других языков, использующих замыкание, ситуация иная).

Пример:

var a = 5;

function testFn() {
alert(a);
}

(function(funArg) {
var a = 20;
funArg();// выведет 5 т.к. в ScopeChain/LexicalEnvironment testFn будет глобальный объект, в котором а = 5
})(testFn)

Другим важным свойство замыкания является следующая ситуация:

var first;
var second;

function testFn() {
var a = 10;

first = function() {
return ++a;
}

second = function() {
return —a;
}

a = 2;
first();//3
}

testFn();

first();//4
second();//3

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

Заключение
В рамках данной статьи мы коротко описали две центральные для EcmaScript концепции: Лексическое окружение и Замыкание. На самом деле обе эти темы гораздо шире. Если у сообщества будет желание получить более углубленное описание различий разных типов лексического окружения или узнать как v8 выстраивает замыкание, то напишите об этом в комментариях.

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