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

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

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


Что такое лексическая область видимости?

Лексическая область видимости это статическая область в 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" выводится в консоль.

Но у нас же нет никакой переменной с именем name в displayName, так что эта функция как-то может получить доступ к переменной своей внешней функции 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. Так как функция сount теперь замыкание, она может получать доступ к переменной counter в функции getCounter, даже после того, как та завершится.

Но обратите внимание, что значение counter не сбрасывается до 0 при каждом вызове count, как вроде бы она должна делать.

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


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

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

Чтобы реально это понять, нам надо разобраться в двумя самыми важными концепциями в JavaScript, а именно, 1) Контекст выполнения и 2) Лексическое окружение.

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

Это абстрактная среда, в которой JavaScript код оценивается и выполняется. Когда выполняется "глобальный" код, он выполняется внутри глобального контекста выполнения, а код функции выполняется внутри контекста выполнения функции.

Тут может быть только один запущенный контекст выполнения (JavaScript - однопоточный язык), который управляется стеком запросов.

Стек выполнения - это стек с принципом LIFO (Последний вошёл, первый вышел), в котором элементы могут быть добавлены или удалены только сверху стека.

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

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

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

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

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

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

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

Лексическое окружение - это структура данных, которая хранит информацию по идентификаторам переменных. Тут идентификатор обозначает имя переменных/функций, а переменная настоящий объект [включая тип функции] или примитивное значение.

У лексического окружения есть два компонента: 1) запись в окружении и 2) отсылка к внешнему окружению.

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

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

lexicalEnvironment = {
	environmentRecord: {
		<identifier> : <value>,
		<identifier> : <value>
	}
	
	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');

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

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

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

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

functionLexicalEnvironment = {
	environmentRecord: {
		b    : 25,
	}
	
	outer: <globalLexicalEnvironment>
}

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

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

Замыкание - это способ получения доступа и управления внешними переменными из функции.

Secrets of the JavaScript Ninja