Что такое Закрытие?

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

Примечание. Свободные переменные — это переменные, которые не объявляются локально и не передаются в качестве параметров.

Пример 1:

function numberGenerator() {
  // Local “free” variable that ends up within the closure
  var num = 1;
  function checkNumber() { 
    console.log(num);
  }
  num++;
  return checkNumber;
}
var number = numberGenerator();
number(); // 2

В приведенном выше примере функция numberGenerator создает локальную «свободную» переменную num (число) и checkNumber (функцию, которая выводит num в консоль).

Функция checkNumber не имеет собственных локальных переменных, однако у нее есть доступ к переменным внутри внешней функции numberGenerator из-за замыкания.

Таким образом, он может использовать переменную num, объявленную в numberGenerator, для успешного вывода его на консоль даже после numberGenerator. вернулся.

Понимание высокого уровня

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

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

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

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

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

В любой момент времени может быть запущен только один контекст выполнения. Вот почему JavaScript является «однопоточным», то есть одновременно может обрабатываться только одна команда.

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

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

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

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

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

Практический пример этой концепции в действии в браузере см. в примере ниже:

var x = 10;
function foo(a) {
  var b = 20;
  function bar(c) {
    var d = 30;
    return boop(x + a + b + c + d);
  }
  function boop(e) {
    return e * -1;
  }
  return bar;
}
var moar = foo(5); // Closure  
/* 
  The function below executes the function bar which was returned 
  when we executed the function foo in the line above. The function bar 
  invokes boop, at which point bar gets suspended and boop gets push 
  onto the top of the call stack (see the screenshot below)
*/
moar(15);

Затем, когда boop возвращается, он извлекается из стека, и bar возобновляется:

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

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

  • Состояние оценки кода: любое состояние, необходимое для выполнения, приостановки и возобновления оценки кода, связанного с этим контекстом выполнения.
  • Функция. Объект функции, который оценивается контекстом выполнения (или null, если оцениваемый контекст является скриптом или модулем).
  • Область. Набор внутренних объектов, глобальная среда ECMAScript, весь код ECMAScript, загружаемый в рамках этой глобальной среды, а также другое связанное состояние и ресурсы.
  • Лексическое окружение. Используется для разрешения ссылок на идентификаторы, сделанные кодом в этом контексте выполнения.
  • Variable Environment: лексическая среда, EnvironmentRecord которой содержит привязки, созданные VariableStatements в этом контексте выполнения.

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

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

Примечание. Технически для реализации замыканий используются как переменная среда, так и лексическая среда. Но для простоты мы обобщим его на среду. Подробное объяснение разницы между лексическим и переменным окружением см. в отличной статье доктора Алекса Раушмайера.

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

По определению:

Лексическая среда — это тип спецификации, используемый для определения ассоциации идентификаторов с конкретными переменными и функциями на основе лексической структуры вложенности кода ECMAScript. Лексическое окружение состоит из записи окружения и, возможно, нулевой ссылки на внешнее лексическое окружение. Обычно лексическое окружение связано с некоторой конкретной синтаксической структурой кода ECMAScript, такой как FunctionDeclaration, BlockStatement или предложение Catch в TryStatement, и новое лексическое окружение создается каждый раз при оценке такого кода. — ECMAScript-262/6.0

Давайте разберем это.

  • "Используется для определения ассоциации идентификаторов":Целью лексической среды является управление данными (т. е. идентификаторами) в коде. Другими словами, он придает смысл идентификаторам. Например, если бы у нас была строка кода «console.log(x / 10)», бессмысленно иметь переменную (или «идентификатор») x без чего-либо который обеспечивает значение для этой переменной. Лексическое окружение предоставляет это значение (или «ассоциацию») через свою запись окружения (см. ниже).
  • «Лексическое окружение состоит из записи окружения»: «Запись окружения» — это причудливый способ сказать, что оно хранит записи обо всех идентификаторах и их привязках, которые существуют в лексическом окружении. Каждое лексическое окружение имеет свою собственную запись окружения.
  • «Лексическая вложенная структура»:Это интересная часть, которая в основном говорит о том, что внутренняя среда ссылается на внешнюю среду, которая ее окружает, и что эта внешняя среда может иметь свою собственную внешнюю среду. В результате среда может служить внешней средой для более чем одной внутренней среды. Глобальная среда — это единственная лексическая среда, не имеющая внешней среды. Язык здесь сложный, поэтому давайте воспользуемся метафорой и подумаем о лексическом окружении как о слоях луковицы: глобальное окружение — это самый внешний слой луковицы; каждый последующий слой ниже вложен внутри.

Абстрактно среда выглядит так в псевдокоде:

LexicalEnvironment = {
  EnvironmentRecord: {
  // Identifier bindings go here
  },
  
  // Reference to the outer environment
  outer: < >
};
  • "Новое лексическое окружение создается каждый раз, когда оценивается такой код": Каждый раз, когда вызывается объемлющая внешняя функция, создается новое лексическое окружение. Это важно — мы еще вернемся к этому моменту в конце. (Примечание: функция — не единственный способ создать лексическое окружение. Другие включают в себя оператор блока или предложение catch. Для простоты в этом посте я сосредоточусь на окружении, созданном функциями)

Короче говоря, каждый контекст выполнения имеет лексическое окружение. Эта лексическая среда содержит переменные и связанные с ними значения, а также имеет ссылку на внешнюю среду.

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

Цепочка областей

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

Давайте посмотрим на пример этой вложенной структуры:

var x = 10;
function foo() {
  var y = 20; // free variable
  function bar() {
    var z = 15; // free variable
    return x + y + z;
  }
  return bar;
}

Как видите, bar вложен в foo. Чтобы помочь вам визуализировать вложение, см. диаграмму ниже:

Мы вернемся к этому примеру позже в посте.

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

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

Объезд: динамическая область видимости против статической области видимости

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

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

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

Пример 1:

var x = 10;
function foo() {
  var y = x + 5;
  return y;
}
 
function bar() {
  var x = 2;
  return foo();
}
 
function main() {
  foo(); // Static scope: 15; Dynamic scope: 15
  bar(); // Static scope: 15; Dynamic scope: 7
  return 0;
}

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

При статической области видимости возвращаемое значение bar основано на значении x на момент создания foo. Это связано со статической и лексической структурой исходного кода, в результате чего x равно 10, а результат равен 15.

Динамическая область, с другой стороны, дает нам стек определений переменных, отслеживаемых во время выполнения — например, то, какой x мы используем, зависит от того, что именно находится в области действия и было определено динамически во время выполнения. Запуск функции bar помещает x = 2 на вершину стека, заставляя foo возвращать 7.

Пример 2:

var myVar = 100;
 
function foo() {
  console.log(myVar);
}
 
foo(); // Static scope: 100; Dynamic scope: 100
 
(function () {
  var myVar = 50;
  foo(); // Static scope: 100; Dynamic scope: 50
})();
// Higher-order function
(function (arg) {
  var myVar = 1500;
  arg();  // Static scope: 100; Dynamic scope: 1500
})(foo);

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

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

Закрытия

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

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

Похоже, что функция «запоминает» эту среду (или область действия), потому что функция буквально имеет ссылку на среду (и переменные, определенные в этой среде)!

Возвращаясь к примеру с вложенной структурой:

var x = 10;
function foo() {
  var y = 20; // free variable
  function bar() {
    var z = 15; // free variable
    return x + y + z;
  }
  return bar;
}
var test = foo();
test(); // 45

Основываясь на нашем понимании того, как работают среды, мы можем сказать, что определения среды для приведенного выше примера выглядят примерно так (обратите внимание, это чисто псевдокод):

GlobalEnvironment = {
  EnvironmentRecord: { 
    // built-in identifiers
    Array: '<func>',
    Object: '<func>',
    // etc..
    
    // custom identifiers
    x: 10
  },
  outer: null
};
 
fooEnvironment = {
  EnvironmentRecord: {
    y: 20,
    bar: '<func>'
  }
  outer: GlobalEnvironment
};
barEnvironment = {
  EnvironmentRecord: {
    z: 15
  }
  outer: fooEnvironment
};

Когда мы вызываем функцию test, мы получаем 45, что является возвращаемым значением при вызове функции bar (поскольку foo вернул bar ). bar имеет доступ к свободной переменной y даже после возврата функции foo, поскольку bar имеет ссылку на y через его внешнюю среду, которая является средой foo! bar также имеет доступ к глобальной переменной x, потому что среда foo имеет доступ к глобальной среде. Это называется «поиск по цепочке областей».

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

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

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

Есть смысл? Хорошо! Теперь, когда мы понимаем внутренности на абстрактном уровне, давайте рассмотрим еще пару примеров:

Пример 1:

Один канонический пример/ошибка — когда есть цикл for, и мы пытаемся связать переменную counter в цикле for с некоторой функцией в цикле for:

var result = [];
 
for (var i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}
result[0](); // 5, expected 0
result[1](); // 5, expected 1
result[2](); // 5, expected 2
result[3](); // 5, expected 3
result[4](); // 5, expected 4

Возвращаясь к тому, что мы только что узнали, становится очень легко обнаружить здесь ошибку! Абстрактно, вот как выглядит среда к моменту выхода из цикла for:

environment: {
  EnvironmentRecord: {
    result: [...],
    i: 5
  },
  outer: null,
}

Неправильным предположением здесь было то, что область видимости различна для всех пяти функций в результирующем массиве. Вместо этого на самом деле происходит то, что среда (или контекст/область действия) одинакова для всех пяти функций в результирующем массиве. Таким образом, каждый раз, когда переменная i увеличивается, она обновляет область действия, которая является общей для всех функций. Вот почему любая из 5 функций, пытающихся получить доступ к i, возвращает 5 (i равно 5, когда цикл for завершается).

Один из способов исправить это — создать дополнительный объемлющий контекст для каждой функции, чтобы каждая из них получила свой собственный контекст/область выполнения:

var result = [];
 
for (var i = 0; i < 5; i++) {
  result[i] = (function inner(x) {
    // additional enclosing context
    return function() {
      console.log(x);
    }
  })(i);
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

Ура! Это исправило :)

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

var result = [];
 
for (let i = 0; i < 5; i++) {
  result[i] = function () {
    console.log(i);
  };
}
result[0](); // 0, expected 0
result[1](); // 1, expected 1
result[2](); // 2, expected 2
result[3](); // 3, expected 3
result[4](); // 4, expected 4

Тада! :)

Пример 2:

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

function iCantThinkOfAName(num, obj) {
  // This array variable, along with the 2 parameters passed in, 
  // are 'captured' by the nested function 'doSomething'
  var array = [1, 2, 3];
  function doSomething(i) {
    num += i;
    array.push(num);
    console.log('num: ' + num);
    console.log('array: ' + array);
    console.log('obj.value: ' + obj.value);
  }
  
  return doSomething;
}
var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // closure #1
var bar = iCantThinkOfAName(6, referenceObject); // closure #2
foo(2); 
/*
  num: 4
  array: 1,2,3,4
  obj.value: 10
*/
bar(2); 
/*
  num: 8
  array: 1,2,3,8
  obj.value: 10
*/
referenceObject.value++;
foo(4);
/*
  num: 8
  array: 1,2,3,4,8
  obj.value: 11
*/
bar(4); 
/*
  num: 12
  array: 1,2,3,8,12
  obj.value: 11
*/

В этом примере мы видим, что каждый вызов функции iCantThinkOfAName создает новое замыкание, а именно foo и bar. Последующие вызовы любой из функций замыкания обновляют переменные замыкания внутри самого замыкания, демонстрируя, что переменные в каждом замыкании по-прежнему могут использоваться iCantThinkOfAName doSomething долгое время после возврата iCantThinkOfAName.

Пример 3:

function mysteriousCalculator(a, b) {
	var mysteriousVariable = 3;
	return {
		add: function() {
			var result = a + b + mysteriousVariable;
			return toFixedTwoPlaces(result);
		},
		
		subtract: function() {
			var result = a - b - mysteriousVariable;
			return toFixedTwoPlaces(result);
		}
	}
}
function toFixedTwoPlaces(value) {
	return value.toFixed(2);
}
var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add() // 15.02
myCalculator.subtract() // 5.00

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

GlobalEnvironment = {
  EnvironmentRecord: { 
    // built-in identifiers
    Array: '<func>',
    Object: '<func>',
    // etc...
    // custom identifiers
    mysteriousCalculator: '<func>',
    toFixedTwoPlaces: '<func>',
  },
  outer: null,
};
 
mysteriousCalculatorEnvironment = {
  EnvironmentRecord: {
    a: 10.01,
    b: 2.01,  
    mysteriousVariable: 3,
  }
  outer: GlobalEnvironment,
};
addEnvironment = {
  EnvironmentRecord: {
    result: 15.02
  }
  outer: mysteriousCalculatorEnvironment,
};
subtractEnvironment = {
  EnvironmentRecord: {
    result: 5.00
  }
  outer: mysteriousCalculatorEnvironment,
};

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

Пример 4:

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

function secretPassword() {
  var password = 'xh38sk';
  return {
    guessPassword: function(guess) {
      if (guess === password) {
        return true;
      } else {
        return false;
      }
    }
  }
}
var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true

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

TL;DR

  • Контекст выполнения — это абстрактное понятие, используемое спецификацией ECMAScript дляотслеживания оценки кода во время выполнения. В любой момент времени может быть только один контекст выполнения, в котором выполняется код.
  • Каждый контекст выполнения имеет лексическое окружение. Эта лексическая среда содержит привязки идентификаторов (т.е. переменные и связанные с ними значения), а также имеет ссылку на свою внешнюю среду.
  • Набор идентификаторов, к которым имеет доступ каждая среда, называется «областью действия». Мы можем вложить эти области в иерархическую цепочку сред, известную как «цепочка областей».
  • У каждой функции есть контекст выполнения, состоящий из лексического окружения, которое придает смысл переменным внутри этой функции, и ссылки на родительское окружение. И поэтому создается впечатление, что функция «запоминает» это окружение (или область видимости), потому что функция буквально имеет ссылку на это окружение. Это закрытие.
  • Замыкание создается каждый раз, когда вызывается объемлющая внешняя функция. Другими словами, внутренней функции не нужно возвращать значение для создания замыкания.
  • Область замыкания в JavaScript является лексической, то есть она определяется статически своим расположением в исходном коде.
  • Замыкания имеют много практических вариантов использования. Одним из важных вариантов использования является поддержка частной ссылки на переменную во внешней области.