Frontend blog.

Замыкание

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

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

В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний (скрытый) объект, называемый лексическим окружением LexicalEnvironment.

Объект лексического окружения состоит из двух частей:

  1. Environment Record — объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this).
  2. Ссылка на внешнее лексическое окружение — то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).

Переменная — это просто свойство специального внутреннего объекта: Environment Record. Получить или изменить переменную, означает, получить или изменить свойство этого объекта.

В этом коде только одно лексическое окружение:

let phrase = 'Hello';
console.log(phrase);

В примере выше, так называемое, глобальное лексическое окружение связанное со всем скриптом. Ссылка на внешнее лексическое окружение будет равна null, так как у глобального окружения нет внешнего окружения.

Вот как оно изменяется при объявлении и присваивании переменной:

// Начало выполнения  // -> <пусто>
let phrase;           // -> { phrase: undefined }
let phrase = 'Hello'; // -> { phrase: "Hello" }
let phrase = 'Bye';   // -> { phrase: "Bye" }

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

  1. В начале скрипта лексическое окружение пустое.
  2. Появляется определение переменной let phrase. У нее нет присвоенного значения, поэтому присваивается undefined.
  3. Переменной phrase присваивается значение.
  4. Переменная phrase меняет значение.

Итого:

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

Function Declaration

До сих пор рассматривались только переменные. Теперь рассмотрим Function Declaration.

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

Для верхнеуровневых функций это означает момент, когда скрипт начал выполнение. Именно поэтому мы можем вызвать функцию, объявленную через Function Declaration, до того, как она определена.

Следующий код демонстрирует, что уже с самого начала в лексическом окружении что-то есть:

// Начало выполнения  // -> { say: function }
let phrase = 'Hello'; // -> { say: function, phrase: "Hello" }

function say(name) {
  console.log(`${phrase}, ${name}`);
};

Внутреннее и внешнее лексическое окружение

В течении вызова say() использует внешнюю переменную phrase.

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

Например, для say('John') это выглядит так:

// global
let phrase = 'Hello';

function say(name) {
  // -> когда выполнение находится здесь
  // создается новое лексическое окружение
  // LexicalEnvironment вызова -> name: 'John', outer: global
  console.log(`${phrase}, ${name}`);
};

say('John'); // -> Hello, John

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

  • Внутреннее лексическое окружение соответствует текущему выполнению say. В нем находится одна переменная name, аргумент функции. Мы вызвали say('John'), так что значение переменной name равное 'John'.
  • Внешнее лексическое окружение — это глобальное лексическое окружение. В нем находятся переменная phrase и сама функция say.

У внутреннего лексического окружения есть ссылка outer на внешнее.

Когда код хочет получить доступ к переменной — сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.

Как происходит поиск в примере выше:

  • Когда console.log внутри say хочет получить доступ к name, он сразу находит переменную в лексическом окружении функции.
  • Когда console.log хочет получить доступ к переменной phrase, которой нет локально, он следует по ссылке на внешнее лексическое окружение и находит переменную там.

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

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

let name = 'John';

function sayHi() {
  console.log(`Hi, ${name}`);
}

name = 'Pete'; // (*)

sayHi(); // -> Hi, Pete

Порядок выполнения кода из примера выше:

  1. В глобальном лексическом окружении есть name: 'John'.
  2. На строке (*) глобальная переменная изменяется, теперь name: 'Pete'.
  3. Момент, когда выполняется функция sayHi() и берет переменную name извне. Теперь из глобального лексического окружения, где переменная уже равна 'Pete'.

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

Лексическое окружение — это специальный внутренний объект. Мы не можем получить его в нашем коде и изменять напрямую.

Вложенные функции

Функция называется «вложенной», когда она создается внутри другой функции.

Например:

function sayHello(firstName, lastName) {

  // вложенная функция
  function getFullName() {
    return firstName + ' ' + lastName;
  }

  console.log('Hello, ' + getFullName());
}

В примере выше вложенная функция getFullName() создана для удобства. Она может получить доступ к внешним переменным и, значит, вывести полное имя.

Что еще интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта, либо сама по себе. И затем может быть использована в любом месте. Не важно где, она все так же будет иметь доступ к внешним переменным.

Например, здесь, вложенная функция присваивается новому объекту в конструкторе:

// функция-конструктор возвращает новый объект
function User(name) {

  // методом объекта становится вложенная функция
  this.sayHi = function () {
    console.log(name);
  }
}

let user = new User('John');
user.sayHi(); // у кода метода 'sayHi' есть доступ к внешней переменной 'name'

А здесь мы просто создаем и возвращаем функцию «счетчик»:

function makeCounter() {
  let count = 0;

  return function () {
    return count++; // есть доступ к внешней переменной 'count'
  }
}

let counter1 = makeCounter();
let counter2 = makeCounter();

console.log(counter1()); // -> 0
console.log(counter1()); // -> 1
console.log(counter1()); // -> 2

console.log(counter2()); // -> 0 (независимые счетчики)
console.log(counter2()); // -> 1
  1. count — локальная переменная функции makeCounter. Мы не можем получить к ней доступ из кода, который не принадлежит makeCounter.
  2. Для каждого вызова makeCounter() создается новое лексическое окружение, со своим собственным count. Функции counter созданные с помощью makeCounter — независимы.

Окружение в деталях

// сокращен предыдущий пример
function makeCounter() {
  let count = 0;
  return () => {
    return count++;
  }
  let counter = makeCounter();
  console.log(counter()); // -> 0
  console.log(counter()); // -> 1
}

Что происходит в примере с makeCounter шаг за шагом:

  1. Когда скрипт только начинает выполняться, есть только глобальное лексическое окружение.

В этот начальный момент есть только функция makeCounter, потому что это Function Declaration. Она еще не выполняется.

Все функции «при рождении» получают скрытое свойство [[Environment]], которое ссылается на лексическое окружение места, где они были созданы.

В данном случае, makeCounter создан в глобальном лексическом окружении, так что [[Environment]] содержит ссылку на него.

Другими словами, функция навсегда запоминает ссылку на лексическое окружение, где она была создана. [[Environment]] — скрытое свойство, которое содержит эту ссылку.

  1. Код продолжает выполняться, объявляется новая глобальная переменная counter, котрой присваиватеся результат вызова makeCounter.

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

Как и все лексические окружения, оно содержит две вещи:

  • Environment Record с локальными переменными. В нашем случае count — единственная локальная переменная.
  • Ссылка на внешнее окружение, которая устанавливается в значение [[Environment]] функции. В данном случае, [[Environment]] функции makeCounter ссылается на глобально окружение.

Теперь у нас есть два лексических окружения: первое — глобальное, второе — для текущего вызова makeCounter, с внешней ссылкой на объект.

  1. В процессе выполнения makeCounter() создается небольшая вложенная функция, которая получает свойство [[Environment]], которое ссылается на лексическое окружение makeCounter() (где она была создана)
  2. Выполнение продолжается, вызов makeCounter() завершается, и результат (вложенная функция) присваивается глобальной переменной counter. У функции counter только одна строчка: return count++, которая будет выполнена, когда функция counter будет вызвана.
  3. При вызове counter() для этого вызова создается новое лексическое окружение. Оно пустое, так как в самом counter локальных переменных нет. Но [[Environment]] counter используется, как ссылка на внешнее лексическое окружение, которое дает доступ к переменным предшествующего вызова makeCounter, где counter был создан.

Теперь, когда вызов ищет переменную count, он сначала ищет в собственном лексическом окружении (пустое), а затем в лексическом окружении предшествующего вызова makeCounter(), где и находит ее.

Хотя makeCounter() закончил выполнение некоторое время назад, его лексическое окружение остается в памяти, потому что есть вложенная функция с [[Environment]], который ссылается на него.

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

  1. Вызов counter() не только возвращает значение count, но и также увеличивает его. Значение count изменяется конкретно в том окружении, где оно было найдено.
  2. Следующие вызовы counter() сделают то же самое.

Еще пример:

function makeWorker() {
  let name = 'Pete';

  return function () {
    console.log(name);
  }
}

let name = 'John';
let work = makeWorker();

work(); // Pete

Функция work() получает name из того места, где была создана, через ссылку на внешнее лексическое окружение.

Но, если бы в makeWorker() не было let name, тогда бы поиск продолжался дальше и была бы взята глобальная переменная. Результатом вызова work() стал бы 'John'.

Конспект статьи из учебника по JavaScriptЗамыкание

вернуться к списку