Автор оригинала: FreeCodeCamp Community Member.
Закрытия являются фундаментальным концепцией JavaScript, что каждый серьезный программист должен знать внутри и выходить.
Интернет упаковывается с большим объяснением «какие» замыкания, но несколько глубоких погружений в «почему» сторона вещей.
Я считаю, что понимание внутренних органов в конечном итоге дает разработчикам более сильным пониманием своих инструментов, поэтому этот пост будет посвящен орехам и болтам Как и Почему закрытие работает так, как они делают.
Надеюсь, вы уйдете лучше, чтобы воспользоваться преимуществами закрытия в своей повседневной работе. Давайте начнем!
Закрытия являются чрезвычайно мощным свойством JavaScript (и большинство языков программирования). Как определено на MDN :
Закрытия функции тот Ссылаться на независимый (бесплатно) переменные Отказ Другими словами, функция определена в закрытии «запоминает» среда, в которой она была создана .
Примечание. Бесплатные переменные являются переменными, которые не являются ни локально объявленными, а также не передаются в качестве параметра.
Давайте посмотрим на некоторые примеры:
Пример 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В приведенном выше примере функция NumbleRator создает локальную «бесплатную» переменную Num (число) и Checknumber . (функция, которая печатает num на консоль).
Функция Checknumber . У нее нет местных переменных – однако у него есть доступ к переменным во внешней функции, Номинатор, из-за закрытия.
Следовательно, он может использовать переменную Num заявлено в Numbrenerator Чтобы успешно войти в консоль Даже после Numbrenerator вернулся.
Пример 2:
В этом примере мы продемонстрируем, что закрытие содержит любые и все локальные переменные, которые были объявлены внутри внешнего ограждения.
function sayHello() {
var say = function() { console.log(hello); }
// Local variable that ends up within the closure
var hello = 'Hello, world!';
return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // 'Hello, world!'Обратите внимание, как переменная Привет определяется после Анонимная функция – но все еще может получить доступ к Привет Переменная. Это потому, что Привет Переменная уже была определена в функции «Область применения» во время создания, что делает его доступным, когда анонимная функция окончательно выполняется.
(Не волнуйтесь, я объясню, что означает «область» означает позже в посте. На данный момент просто катитесь с ним!)
Понимание высокого уровня
Эти примеры проиллюстрированы «какие» замыкания находятся на высоком уровне. Общая тема это: У нас есть доступ к переменным, определенным в ограждающих функциях (ы) даже после прилагающей функции, которая определяет эти переменные, вернули Отказ
Очевидно, что-то происходит в фоновом режиме, что позволяет эти переменные до сих пор быть доступны долго после прилагающей функции, которая определена их, вернулась.
Чтобы понять, как это возможно, нам нужно будет прикоснуться к нескольким связанным понятиям – начиная с 3000 футов и медленно поднимаясь на пути к земле закрытия. Давайте начнем с всеобъемлющего контекст Внутри, в которой работает функция, известная как «Контекст исполнения» Отказ
Контекст выполнения – это абстрактная концепция, используемая спецификацией 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); Тогда когда буп Возвращает, это выскочит с стека и бар возобновляется:
Когда у нас есть куча выполнения контекстов, работающих один за другим – часто приостановившись в середине, а затем позже возобновился – нам нужен способ отслеживать состояние, чтобы мы могли управлять заказом и выполнением этих контекстов.
И это на самом деле дело. Согласно спецификации eCmascript каждый контекст выполнения имеет различные компоненты состояния, которые используются для отслеживания прогресса, который сделал код в каждом контексте. К ним относятся:
- Состояние оценки кода: Любое состояние, необходимое для выполнения, приостановки, и возобновить оценку кода, связанного с этим контекстом выполнения
- Функция: Функциональный объект, который контекст выполнения оценивает (или NULL, если оценивается контекст, является A Script или модуль )
- Царство: Набор внутренних объектов, глобальная среда Ecmascript, весь код ECMAScript, который загружен в рамках объема этой глобальной среды, а также другое связанное состояние и ресурсы
- Лексическая среда: Используется для разрешения ссылок идентификатора, выполненных кодом в этом контексте выполнения.
- Переменная среда: Лексическая среда, окружающая среда которой содержит привязки, созданные вариациями в этом контексте выполнения.
Если это звучит слишком запутанному вам, не волнуйтесь. Из всех этих переменных переменная лексической среды – это самая интересная для нас, потому что она явно заявляет, что она решает «Идентификаторы ссылки» сделанный кодом в этом контексте выполнения.
Вы можете подумать о «идентификаторах» в качестве переменных. Поскольку наша оригинальная цель состояла в том, чтобы выяснить, как мы к магическим переменным доступа, даже после того, как функция (или «контекст») вернулась, лексическая среда выглядит как то, что мы должны копать!
Примечание : Технически, как вариабельная среда, так и лексическая среда используются для реализации закрытия. Но для простоты мы обобщем это в «окружающей среде». Для подробного объяснения разницы между лексической и переменной окружающей средой см. Превосходное доктор Алекс Раушмайер Статья Отказ
По определению:
Давайте сломаемся.
- «Используется для определения ассоциации идентификаторов»: Целью лексической среды является управление данными (I.E. Identiers) в коде. Другими словами, это придает смысл идентификаторам. Например, если у нас была строка кода ” console.log (x/10)”, Это бессмысленно иметь переменную (или «идентификатор») х без чего-то, что обеспечивает смысл для этой переменной. Лексическая среда предоставляет это значение (или «ассоциацию») через запись среды (см. Ниже).
- «Лексическая среда состоит из записи окружающей среды»: Регистрация среды – это модный способ сказать, что он сохраняет запись всех идентификаторов и их привязки, которые существуют в лексической среде. Каждая лексическая среда имеет свою собственную регистрацию окружающей среды.
- «Лексическая структура гнездования»: Это интересная часть, которая в основном говорит, что внутренняя среда ссылается на внешнюю среду, которая ее окружает, и что эта внешняя среда также может иметь собственную внешнюю среду. В результате среда может служить внешней средой для более чем одной внутренней среды. Глобальная среда – единственная лексическая среда, которая не имеет внешней среды. Язык здесь хитрый, поэтому давайте будем использовать метафору и подумать о лексических средах, таких как слои лука: глобальная среда является внешним слоем лука; Каждый последующий слой ниже вложен внутри.
Абстрактно, окружающая среда выглядит в псевдокоде:
LexicalEnvironment = {
EnvironmentRecord: {
// Identifier bindings go here
},
// Reference to the outer environment
outer: < >
};- «Новая лексическая среда создается каждый раз, когда такой код оценивается»: Каждый раз вызывается приключенная внешняя функция, создается новая лексическая среда. Это важно – мы вернемся к этому точку снова в конце. (Боковая заметка: функция – это не единственный способ создать лексику. Другие включают в себя блок-оператор или пункт доловов. Для простоты, я сосредоточусь на окружающей среде, созданной функциями на протяжении всего этого поста)
Короче говоря, каждый контекст выполнения имеет лексическую среду. Эта лексическая среда содержит переменные и связанные с ними значениями, а также имеет ссылку на ее внешнюю среду.
Лексическая среда может быть глобальной средой, средой модуля (которая содержит привязки для деклараций верхнего уровня модуля) или функциональной среды (среда, созданная из-за вызова функции).
На основании вышеуказанного определения мы знаем, что окружающая среда имеет доступ к окружающей среде его родителей, и его родительская среда имеет доступ к своей родительской среде и так далее. Этот набор идентификаторов, которые у каждой среды есть доступ, называется “объем.” Мы можем гнездиться примерно на иерархическую цепочку среды, известных как «Цепочка сферы» Отказ
Давайте посмотрим на пример этой структуры гнездования:
var x = 10;
function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return 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;
}Выше видим, что статический охват и динамический объем возвращает разные значения, когда функциональная панель вызывается.
Со статическими объемами, возвращаемой стоимостью бар основан на значении х во время Foo Создание. Это из-за статической и лексической структуры исходного кода, что приводит к х Быть 10 и результат – 15.
Динамическая область, с другой стороны, дает нам стопку переменных определений, отслеживаемых во время выполнения – такое, что х Мы используем, зависит от того, что именно находится в области охвата и определяется динамически во время выполнения. Запуск функции бар толкает на верхнюю часть стека, делая Foo return 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: '',
Object: '',
// etc..
// custom identifiers
x: 10
},
outer: null
};
fooEnvironment = {
EnvironmentRecord: {
y: 20,
bar: ''
}
outer: GlobalEnvironment
};
barEnvironment = {
EnvironmentRecord: {
z: 15
}
outer: fooEnvironment
}; Когда мы вызываем функцию Тест , мы получаем 45, что является возвращаемой стоимостью от вызвании функции бар (потому что Foo вернулся бар ). бар имеет доступ к свободной переменной y Даже после функции Foo вернулся, потому что бар имеет ссылку на y через его внешнюю среду, которая является Foo Окружающая среда! бар Также имеет доступ к глобальной переменной х потому что Foo Охрана имеет доступ к глобальной среде. Это называется «Сфера цепочки».
Возвращаясь к нашему обсуждению динамического объема VS статического применения: для внедрения замыкания мы не можем использовать динамическую проверку через динамический стек для хранения наших переменных.
Причина в том, что это будет означать, что когда функция возвращается, переменные будут выскакиваться с стека и больше не доступны – что противоречит нашему первоначальному определению закрытия.
То, что происходит вместо этого, состоит в том, что данные окрашивания родительского контекста сохраняются в том, что известно как «куча», которая позволяет сохранять данные после того, как вызов функции, который сделал их возвратами (т. Е. Даже после того, как контекст выполнения отключен от выполнения стек вызовов).
Иметь смысл? Хороший! Теперь, когда мы понимаем внутренние органы на абстрактном уровне, давайте посмотрим на пару образцов:
Пример 1:
Один канонический пример/ошибка заключается в том, когда есть для цикла, и мы пытаемся связывать переменную счетчика в контуре с какой-то функцией в фолке:
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-Loop выходит:
environment: {
EnvironmentRecord: {
result: [...],
i: 5
},
outer: null,
}Неправильное предположение здесь было то, что объем отличается для всех пяти функций в пределах массива результатов. Вместо этого на самом деле происходит то, что окружающая среда (или контекст/область действия) одинакова для всех пяти функций в пределах массива результатов. Поэтому каждый раз, когда переменная Я увеличивается, он обновляет объем – который передается всеми функциями. Вот почему любая из 5 функций, пытающихся доступа к Я Возврат 5 (I равно 5, когда выходы на петли).
Один из способов исправления это – создать дополнительный ограждающий контекст для каждой функции, чтобы каждый из них получил собственное контекст выполнения/область применения:
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Ура! Это исправило это:)
Другой, довольно умный подход – использовать Пусть вместо var , поскольку Пусть Для каждой итерации на каждой итерации создается новая связывание идентификатора для каждой итерации:
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
*/В этом примере мы можем видеть, что каждый вызов функции ICANTTHINCINCOFANAME Создает новое закрытие, а именно Foo и бар Отказ Последующие вызовы для любого замыкания функций обновляют переменные замыкания в этом закрытии, демонстрируя, что переменные в каждый Закрытие продолжается полезным для ICANTTHINCINCOFANAME ‘s Досметочное Функция длина после ICANTTHINCINCOFANAME возвращается.
Пример 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То, что мы можем наблюдать, что Таинственный клеток находится в глобальном объеме, и он возвращает две функции. Абстрактно, окружающая среда для приведенного выше примера выглядит так:
GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '',
Object: '',
// etc...
// custom identifiers
mysteriousCalculator: '',
toFixedTwoPlaces: '',
},
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,
}; Потому что наш Добавить и вычесть Функции имеют ссылку на Таинственный клеток Функциональная среда, они могут воспользоваться переменными в этой среде, чтобы рассчитать результат.
Пример 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Это очень мощная техника – это дает функцию закрытия Догадка Эксклюзивный доступ к пароль переменная, что делает невозможным доступ к пароль снаружи.
- Контекст выполнения – это абстрактная концепция, используемая спецификацией Ecmascript для Отслеживайте оценку времени выполнения кода. В любой момент времени может быть только один контекст выполнения, который выполняет код.
- Каждый контекст выполнения имеет лексическую среду. Эта лексическая среда удерживает привязки идентификатора (то есть переменные и связанные с ними значения), а также имеет ссылку на ее внешнюю среду.
- Набор идентификаторов, которые в каждой среде есть доступ, называется «Область». Мы можем закрывать эти области в иерархическую цепочку сред, известную как «цепь области».
- Каждая функция имеет контекст выполнения, который содержит лексическую среду, которая дает значение переменным в этой функции и ссылке на окружающую среду его родителей. И поэтому он выглядит как будто функция «запоминает» эту среду (или объем), потому что функция буквально имеет ссылку на эту среду. Это закрытие.
- Закрытие создается каждый раз, когда вызывается ограждающая внешняя функция. Другими словами, внутренняя функция не должна возвращаться для создания закрытия.
- Область замыкания в JavaScript является лексическим, что означает, что он определяется статически по его расположению в исходном коде.
- Закрытия имеют много практических случаев использования. Одно важное значение использования – поддерживать личную ссылку на переменную во внешнем объеме.
Я надеюсь, что этот пост был полезен и дал вам ментальную модель для того, как в JavaScript реализованы закрытия. Как видите, понимая орехи и болты того, как они работают, указывает на гораздо более легкую замену – не говоря уже о сохранении много головной боли, когда пришло время отлаживать.
PS: Я человек и делаю ошибки – так что если вы найдете какие-либо ошибки, я бы понравился для вас, чтобы дать мне знать!
Дальнейшее чтение
Ради краткости я оставил несколько тем, которые могут быть интересными для некоторых читателей. Вот некоторые ссылки, которые я хотел поделиться:
- Какая вариабельная среда в контексте исполнения? Доктор Аксель Раушмайер делает явление, объясняя ее, поэтому я уйду с ссылкой на свой блог пост: http://www.2aless.com/2011/04/ecmascript-5-spec-lexalEnvironment.html.
- Каковы различные типы регионов окружающей среды? Прочитайте спецификацию здесь: http://www.ecma-innernational.org/ecma-262/6.0/#sec-environment-records.
- Отличная статья MDN на закрытии: https://developer.mozilla.org/en-us/docs/web/javascript/Closures.
- Другие? Пожалуйста, предложите, и я добавлю их!