[Перевод] Замыкания в JavaScript для начинающих

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

Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.

652808540f60e3a5f1b4b255fe90337a.png


Что такое замыкание?


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

Что такое лексическое окружение?


Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:

let a = 'global';
  function outer() {
    let b = 'outer';
    function inner() {
      let c = 'inner'
      console.log(c);   // 'inner'
      console.log(b);   // 'outer'
      console.log(a);   // 'global'
    }
    console.log(a);     // 'global'
    console.log(b);     // 'outer'
    inner();
  }
outer();
console.log(a);         // 'global'


Здесь у функции inner() есть доступ к переменным, объявленным в её собственной области видимости, в области видимости функции outer() и в глобальной области видимости. Функция outer() имеет доступ к переменным, объявленным в её собственной области видимости и в глобальной области видимости.

Цепочка областей видимости вышеприведённого кода будет выглядеть так:

Global {
  outer {
    inner
  }
}


Обратите внимание на то, что функция inner() окружена лексическим окружением функции outer(), которая, в свою очередь, окружена глобальной областью видимости. Именно поэтому функция inner() может получить доступ к переменным, объявленным в функции outer() и в глобальной области видимости.

Практические примеры замыканий


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

▍Пример №1

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


Здесь мы вызываем функцию person(), которая возвращает внутреннюю функцию displayName(), и сохраняем эту функцию в переменной peter. Когда мы, после этого, вызываем функцию peter() (соответствующая переменная, на самом деле, хранит ссылку на функцию displayName()), в консоль выводится имя Peter.

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

▍Пример №2

function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2


Тут, как и в предыдущем примере, мы храним ссылку на анонимную внутреннюю функцию, возвращённую функцией getCounter(), в переменной count. Так как функция count() представляет собой замыкание, она может обращаться к переменной counter функции getCount() даже после того, как функция getCounter() завершила работу.

Обратите внимание на то, что значение переменной counter не сбрасывается в 0 при каждом вызове функции count(). Может показаться, что оно должно сбрасываться в 0, как могло бы быть при вызове обычной функции, но этого не происходит.

Всё работает именно так из-за того, что при каждом вызове функции count() для неё создаётся новая область видимости, но существует лишь одна область видимости для функции getCounter(). Так как переменная counter объявлена в области видимости функции getCounter(), её значение между вызовами функции count() сохраняется, не сбрасываясь в 0.

Как работают замыкания?


До сих пор мы говорили о том, что такое замыкания, и рассматривали практические примеры. Теперь поговорим о внутренних механизмах JavaScript, обеспечивающих их работу.

Для того чтобы понять замыкания, нам нужно разобраться с двумя важнейшими концепциями JavaScript. Это — контекст выполнения (Execution Context) и лексическое окружение (Lexical Environment).

▍Контекст выполнения


Контекст выполнения — это абстрактное окружение, в котором вычисляется и выполняется JavaScript-код. Когда выполняется глобальный код, это происходит внутри глобального контекста выполнения. Код функции выполняется внутри контекста выполнения функции.

В некий момент времени может выполняться код лишь в одном контексте выполнения (JavaScript — однопоточный язык программирования). Управление этими процессами ведётся с использованием так называемого стека вызовов (Call Stack).

Стек вызовов — это структура данных, устроенная по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Новые элементы можно помещать только в верхнюю часть стека, и только из неё же элементы можно изымать.

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

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

e2304def39694bb52330011b2f0fa4af.png


Пример контекста выполнения

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

Стек вызовов этого кода выглядит так:

61286c7d24769fa8df93b030e4d98c6b.png


Стек вызовов

Когда завершается выполнение функции first(), её контекст выполнения извлекается из стека вызовов и управление передаётся контексту выполнения, находящемуся ниже его, то есть — глобальному контексту. После этого будет выполнен оставшийся в глобальной области видимости код.

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


Каждый раз, когда JS-движок создаёт контекст выполнения для выполнения функции или глобального кода, он создаёт и новое лексическое окружение для хранения переменных, объявляемых в этой функции в процессе её выполнения.

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

Лексическое окружение содержит два компонента:

  • Запись окружения (environment record) — место, где хранятся объявления переменных и функций.
  • Ссылка на внешнее окружение (reference to the outer environment) — ссылка, позволяющая обращаться к внешнему (родительскому) лексическому окружению. Это — самый важный компонент, с которым нужно разобраться для того, чтобы понять замыкания.


Концептуально лексическое окружение выглядит так:

lexicalEnvironment = {
  environmentRecord: {
     : ,
     : 
  }
  outer: < Reference to the parent lexical environment>
}


Взглянем на следующий фрагмент кода:

let a = 'Hello World!';
function first() {
  let b = 25;  
  console.log('Inside first function');
}
first();
console.log('Inside global execution context');


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

globalLexicalEnvironment = {
  environmentRecord: {
      a     : 'Hello World!',
      first : < reference to function object >
  }
  outer: null
}


Обратите внимание на то, что ссылка на внешнее лексическое окружение (outer) установлена в значение null, так как у глобальной области видимости нет внешнего лексического окружения.

Когда движок создаёт контекст выполнения для функции first(), он создаёт и лексическое окружение для хранения переменных, объявленных в этой функции в ходе её выполнения. В результате лексическое окружение функции будет выглядеть так:

functionLexicalEnvironment = {
  environmentRecord: {
      b    : 25,
  }
  outer: 
}


Ссылка на внешнее лексическое окружение функции установлена в значение , так как в исходном коде код функции находится в глобальной области видимости.

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

Подробный разбор примеров работы с замыканиями


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

▍Пример №1


Взгляните на данный фрагмент кода:

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


Когда выполняется функция person(), JS-движок создаёт новый контекст выполнения и новое лексическое окружение для этой функции. Завершая работу, функция возвращает функцию displayName(), в переменную peter записывается ссылка на эту функцию.

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

personLexicalEnvironment = {
  environmentRecord: {
    name : 'Peter',
    displayName: < displayName function reference>
  }
  outer: 
}


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

Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName()), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:

displayNameLexicalEnvironment = {
  environmentRecord: {
    
  }
  outer: 
}


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

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

▍Пример №2

function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2


Лексическое окружение функции getCounter() будет выглядеть так:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 0,
     : < reference to function>
  }
  outer: 
}


Эта функция возвращает анонимную функцию, которая назначается переменной count.

Когда выполняется функция count(), её лексическое окружение выглядит так:

countLexicalEnvironment = {
  environmentRecord: {
  
  }
  outer: 
}


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

Движок находит переменную, выводит её в консоль и инкрементирует переменную counter, хранящуюся в лексическом окружении функции getCounter().

В результате лексическое окружение функции getCounter() после первого вызова функции count() будет выглядеть так:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 1,
     : < reference to function>
  }
  outer: 
}


При каждом следующем вызове функции count() JavaScript-движок создаёт новое лексическое окружение для этой функции и инкрементирует переменную counter, что приводит к изменениям в лексическом окружении функции getCounter().

Итоги


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

Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru