Что такое замыкание в javascript
Замыкания в JavaScript используются довольно часто, и вы, наверняка, уже сталкивались с ними. Они позволяют делать код более выразительным и лаконичным. В какой-то степени, замыкания могут показаться сложной темой, но в данной статье мы постараемся разобраться с ними. Перед знакомством с этой темой необходимо хорошо понимать, какие особенности имеет область видимости переменных в JavaScript.
Что такое замыкание
Замыкание — это функция, объявленная внутри другой функции и имеющая доступ к переменным внешней (вмещающей) функции. Замыкание имеет доступ сразу к трем областям видимости:
- к своей собственной области видимости (переменные, объявленные внутри замыкания);
- к области видимости внешней функции (переменные, объявленные внутри внешней функции);
- к глобальной области видимости.
Внутренняя функция имеет доступ не только к переменным внешней функции, но и к параметрам внешней функции. Обратите внимание, что внутренняя функция не может использовать объект arguments внешней функции, однако, имеет доступ к параметрам внешней функции напрямую.
Простыми словами замыкание — это функция, описанная внутри другой функции.
Пример замыкания в JavaScript:
function showName(firstName, lastName) < var nameIntro = "Your name is "; function makeFullName() < return nameIntro + firstName + " " + lastName; >return makeFullName(); > showName("Michael", "Jackson"); // Your name is Michael Jackson
Замыкания также нередко используются в jQuery:
$(function() < var selections = []; $('.niners').click(function() < selections.push(this.prop('name')); >); >);
Правила и побочные эффекты замыканий
Замыкание имеет доступ к переменным внешней функции даже после ее выполнения.
Одним из наиболее тонких моментов, связанных с замыканиями, является то, что внутренняя функция продолжает иметь доступ к области видимости внешней даже после того, как внешняя выполнилась.
Это означает, что даже после того, как внешняя функция выполнилась, внутренняя функция все еще может быть вызвана и она все еще имеет доступ к переменным внешней функции.
Замыкания хранят ссылки на переменные внешней функции, а не фактические значения.
Такая интересная особенность позволяет описывать приватные переменные. Это способ впервые был предложен Дугласом Крокфордом:
function user() < var name = 'Unknown'; return < getName: function() < return name; >, setName: function(newName) < name = newName; >> > var testUser = user(); testUser.getName(); // Unknown testUser.setName('John Smith'); // Изменяем значение приватной переменной testUser.getName(); // John Smith
Побочные эффекты, связанные с замыканиями
Т.к. замыкание хранит ссылку на переменную внешней функции, а не фактическое значение, могут возникать побочные эффекты, связанные с тем, что замыкание изменяет внешнюю переменную:
function userIdGenerator(users) < var i; var uniqueId = 100; for (i = 0; i < users.length; i++) < users[i]['id'] = function() < return uniqueId + i; >> return users; > var testUsers = [< name: "Smith", id:0 >, < name: "Johnson", id:0 >, < name: "Thompson", id:0 >]; var testUsersIds = userIdGenerator(testUsers); var firstId = testUsersIds[0]; console.log(firstId.id()); // 103
В предыдущем примере, к тому времени, когда вызывается анонимная функция, значение i становится равно 3 (длина массива). Число 3 было прибавлено к значению переменной uniqueId, тем самым для всех элементов массива testUsers значение id стало равно 103, вместо предполагаемых 100, 101, 102.
Решением предыдущей проблемы может быть шаблон немедленно вызываемой функции (Immediately Invoked Function Expression — IIFE):
function userIdGenerator(users) < var i; var uniqueId = 100; for (i = 0; i < users.length; i++) < users[i]['id'] = function(j) < return function() < return uniqueId + j; >(); > (i); > return users; > var testUsers = [< name: "Smith", id:0 >, < name: "Johnson", id:0 >, < name: "Thompson", id:0 >]; var testUsersIds = userIdGenerator(testUsers); var firstId = testUsersIds[0]; console.log(firstId.id); // 100 var secondId = testUsersIds[1]; console.log(secondId.id); // 101
Заключение
Замыкания — это очень интересный и крайне полезный прием программирования. Суть замыкания можно выразить так: в функции доступны все переменные той области видимости, в которой она была определена.
Замыкания позволяют реализовывать аналог приватных переменных, минимизировать выход переменных в глобальную область видимости. С другой стороны, неаккуратное использование замыканий может привести к побочным эффектам, описанным выше. Поэтому использовать их стоит крайне внимательно.
что такое замыкание js
«Замыкание» — это способность функции запоминать переменные, которые были определены внутри родительской функции, даже после того, как родительская функция была выполнена.
function createCounter() // переменная, которую нужно запомнить let count = 0; function counter() // увеличиваем нашу запомненную переменную count++; console.log(count); > // возвращаем функцию return counter; > // создаем новую функцию (с замыканием) const incrementCounter = createCounter(); incrementCounter(); // 1 incrementCounter(); // 2 incrementCounter(); // 3
В этом примере мы создали функцию createCounter() , которая создает другую функцию counter() . Внутри функции мы создали переменную count , которая была определена внутри родительской функции. Функция counter() возвращает значение count , увеличивая его на 1 . Когда мы вызываем createCounter() , она возвращает функцию counter() , которая имеет доступ к count благодаря замыканию. Каждый раз, когда мы вызываем incrementCounter() ,
Если мы создадим новый счетчик с помощью функции createCounter() , то отсчет для него начнется заново.
// Создали еще одну новую функци-счетчик const newIncrementCounter = createCounter(); newIncrementCounter(); // 1 newIncrementCounter(); // 2 newIncrementCounter(); // 3
Что такое замыкания в JavaScript и как они работают
Сегодня мы разберём что же такое замыкания в JavaScript и как они работают. Это тема, в которой теряются не только новички, но и даже более опытные разработчики. Поэтому предлагаю разобрать её детально.
Пример работы замыканий
Введём сначала функцию высшего порядка changeBalance(). Это функция, которая возвращает другую функцию:
Внутри объявим переменную balance, которая изначально будет равна 0:
И вернём из этой функции новую анонимную функцию, в которую мы передадим аргументом sum и в результате изменим balance на эту сумму:
Теперь посмотрим на ту магию, которая нам позволяет делать замыкание в случае такого использовании функции.
Создадим константу change, которая будет вызовом функции changeBalance(). change теперь станет функцией, которую мы описали выше. То есть функцией, которая примет sum и поменяет balance:
Давайте теперь посмотрим на её поведение, когда попытаемся вызвать эту функцию. Чтобы посмотреть на результат после изменения баланса выведем его в консоль:
Посмотрим что же будет происходить. Вызываем change и передаем ей параметры:
Вывод в консоль:
И вот тут начинает работать замыкание. Несмотря на то, что мы уже вернули функцию и она находится в глобальном контексте, мы все равно можем изменять внутри переменную balance. И более того, от вызова к вызову, наш баланс сохраняется и мы можем его менять.
Теперь на этом примере детально рассмотрим как все это работает по шагам. И как раз поймем где же находится это замыкание и что это.
Пошаговая работа замыканий
В рамках первого шага, в глобальном scope лежит функция changeBalance и константа change, а stack содержит только глобальный контекст.
Теперь начинаем исполнять код: объявляем функцию и переходим к выполнению функции changeBalance():
Что происходит на stack? Когда начинаем выполнять функцию в неё помещается changeBalance():
- changeBalance()
- global
- Global → changeBalance
То есть эта функция при выполнении объявляет баланс и возвращает новую функцию. На этом исполнение changeBalance() заканчивается.
С точки зрения scope у нас есть scope под названием changeBalance, в котором определен balance. Две вещи, которые изменились: на stack поместился changeBalance() и в scope появился changeBalance — дочерний scope относительно глобального.
Теперь перейдем к шагу три, где мы уже вызываем наш change():
- change()
- global
- Global → changeBalance
- Global → change
И тут появятся вопрос, что change никак не сможет достучаться до balance в changeBalance, потому что они находятся на одном уровне. Так почему все ещё можно изменить баланс? На самом деле он не меняет balance в changeBalance. Он меняет balance, который находится в рамках нашего замыкания. По сути, когда мы вызываем change, он как бы носит за собой контекст своего создания, и, когда происходит вызов функции, он знает что balance в нём есть. Это фактически дополнительная переменная scope, которая у него присутствует. Вот эта связь, которая реализуется внутренними механиками JavaScript и называется замыканием.
При этом, в отличие от объявлений функции, замыканиями мы никак сами управлять не можем. Это механика, которая просто работает под капотом. Более того, извне мы никак до переменной balance достучаться не сможем. Единственный, кто имеет к ней доступ — это change.
Что же такое замыкание?
Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена.
Простыми словами: функция помнит в каком контексте она была создана и может его использовать.
changeBalance является лексическим окружением, она становится неразрывной частью change.
При этом важно понимать и помнить, что замыкание всегда имеет более высокий приоритет по сравнению с переменными родительских scope. Если мы в глобальных переменных объявим еще один balance, то он изменён не будет, потому что первостепенно функция change посмотрит своё замыкание, и, если есть в этом замыкании такая переменная, она будет изменена. Если нет, то она пойдет по цепочке искать этот balance во вне.
JavaScript с нуля — основы языка и практика для начинающих
— 18 часов коротких лекций по 10 — 15 минут
— 30 упражнений для закрепления на практике
— 14 тестов для проверки знаний
— Рейтинг ⭐ 4.9 на основании отзывов
— 30-ти дневная гарантия возврата денег
Замыкания в JavaScript
Это перевод статьи Притти Кассириди «Let’s Learn JavaScript Closures».
Замыкания являются ключевой особенностью в JavaScript, которую каждый уважающий себя программист должен знать.
В интернете есть множество объяснений того, что из себя представляют замыкания, но мало кто рассказывает, почему они такие.
Разработчикам понимание принципов работы тех или иных особенностей языка, даст больше осознанности в их применении, поэтому этот пост посвящён внутреннему устройству замыканий: как они работают и почему.
Надеюсь, после этой статьи вы будете лучше подготовлены к использованию замыканий в своей повседневной разработке. Давайте начнём.
Что такое замыкания?
Замыкания являются мощным инструментом в JavaScript и других языках программирования. Вот определение с MDN:
Замыкания — это функции, ссылающиеся на независимые (свободные) переменные. Другими словами, функция, определённая в замыкании, «запоминает» окружение, в котором она была создана.
Заметка: свободные переменные — это переменные, которые не объявлены локально и не передаются в качестве параметра.
Давайте посмотрим на несколько примеров:
Пример 1
function numberGenerator() < // Локальная «свободная» переменная, которая доступна только в замыкании var num = 1; function checkNumber() < console.log(num); >num++; return checkNumber; > var number = numberGenerator(); number(); // 2
В примере функция numberGenerator создаёт локальную «свободную» переменную num (число) и checkNumber (функция, которая выводит число в консоль). Функция checkNumber не содержит собственной локальной переменной, но благодаря замыканию она имеет доступ к переменным внутри внешней функции, numberGenerator. Поэтому объявленная в numberGenerator переменная num будет успешно выведена в консоль, даже после того, как numberGenerator вернёт результат выполнения.
Пример 2
В этом примере видно, что замыкания содержат в себе все локальные переменные, которые были объявлены внутри внешней замкнутой функции — enclosing function.
function sayHello() < var say = function() < console.log(hello); >// Локальная переменная, которая доступна только в замыкании var hello = 'Hello, world!'; return say; > var sayHelloClosure = sayHello(); sayHelloClosure(); // ‘Hello, world!’
Обратите внимание, как переменная hello определяется после анонимной функции, но эта функция всё равно может получить доступ к этой переменной hello. Это происходит из-за того, что переменная hello во время создания уже была определена в области видимости (scope), тем самым сделав её доступной на тот момент, когда анонимная функция будет выполнена. Не беспокойтесь, позже я объясню, что такое «область видимости». А пока просто смиритесь с этим.
Понимаем высокий уровень
Наши примеры показали нам, почему замыкания находятся на высоком уровне. Главная мысль такая: мы имеем доступ к переменным, которые были определены в замкнутых функциях и описывают их переменные как возвращённые. Также что-то происходит в фоновом режиме, что делает эти переменные доступными после замкнутых функций, которые определяют и возвращают их.
Чтобы понять, как это работает, давайте рассмотрим несколько связанных между собой идей. Мы зайдём издалека и постепенно вернёмся к замыканиям. Начнём наш путь с общего контекста, в котором выполняется функция, и известного как контекст выполнения — execution context.
Контекст выполнения
Контекст выполнения — это абстрактное понятие, которое используется в спецификации ECMAScript для оценки времени выполнения кода. Это может быть глобальный контекст — global context, в котором ваш код выполнится первым, или когда поток выполнения переходит в тело функции.
В любой момент времени выполняется только один контекст функции (тело функции). Вот почему JavaScript является однопотоковым, так как единовременно может выполняться только одна команда. Обычно браузеры поддерживают этот контекст с помощью стека — stack. Стек — структура данных, выполняемая в обратном порядке: 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); // Замыкание /* Функция ниже выполняет функцию bar, которая была возвращена в тот момент, когда мы выполнили функцию foo в строке выше. Функция bar вызывает boop, в этот момент bar получает приостановку и boop перемещается на вершину стека вызовов(call stack) (см. скриншот ниже) */ moar(15);

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

Когда у нас есть целая куча контекстов исполнения, выполняющиеся один за другим и останавливающиеся в середине выполнения и снова запускающиеся, то нужно как-то отслеживать их состояние, чтобы мы могли управлять последовательностью выполнения этих контекстов. Согласно спецификации ECMAScript, каждый контекст выполнения имеет различные компоненты, которые используются для отслеживания прогресса исполнения кода. К ним относятся:
- Оценка состояния кода — любое состояние необходимо выполнить, приостановить и возобновить определение кода, связанного с этим контекстом выполнения.
- Функция — объект функции, который оценивает контекст выполнения или null, если контекст был определён как script или модуль.
- Область — набор внутренних объектов, глобальное окружение ECMAScript, весь код ECMAScript, который находится в пределах этого глобального окружения и другие связанные с ним состояния и ресурсы.
- Лексическое окружение — используется для разрешения ссылок идентификатора кода в этом контексте исполнения.
- Переменное окружение — лексическое окружение, чья запись окружения — EnvironmentRecord имеет связи, созданные заявленными переменными — VariableStatements в этом контексте выполнения.
Не волнуйтесь, если это звучит слишком сложно. Из всех переменных, переменные лексического окружения наиболее интересны для нас, ведь они явно указывают, что принимают идентификатор ссылки кода в этом контексте выполнения. Вы можете думать о «идентификаторах» как о переменных. Так как наша первоначальная цель состояла в том, чтобы выяснить, как это возможно, что мы получаем доступ к переменным даже после того, как функция или «контекст» была возвращена, то мы должны копнуть ещё глубже в лексическую область видимости.
Заметка: c технической точки зрения, окружение переменных и лексическая область видимости используются для реализации замыканий. Но для простоты мы заменим его на «окружение». Для детального объяснения разницы между лексическим и переменным окружением читайте статью Акселя Раушмайера.
Лексическая область видимости
Дадим определение: лексическое окружение — специфичный тип, используемый для связи идентификаторов с определёнными переменными и функциями на основе лексической структуры вложенности кода ECMAScript. Лексическое окружение состоит из записи окружения и, возможно, нулевой ссылки на внешнее лексическое окружение. Обычно лексическое окружение связано с определённой синтаксической структурой, например: FunctionDeclaration — объявление функции, BlockStatement — оператор блока, Catch clause — условный оператор, TryStatement — перехват ошибок и новым лексическим окружением, которое создавалось каждый раз при разборе кода. — ECMAScript-262/6.0
Давайте разберём это.

- **«используемый для связи идентификаторов»: целью лексического окружения является управление данными, то есть идентификаторами в коде. Говоря иначе, это придаёт им смысл. Например, у нас есть такая строка в консоли: console.log (x / 10), x** здесь бессмысленная переменная или идентификатор без чего-либо, что придавало бы ей смысл. Лексическое окружение обеспечивает смысл или «ассоциацию» через запись окружения. Смотрите ниже.
- Лексическое окружение состоит из записи окружения: запись окружения — причудливый способ сказать, что она хранит записи всех идентификаторов и связей, которые существуют в лексической области видимости. Каждая лексическая область видимости имеет собственную запись окружения.
- Лексическая структура вложенности: самый интересный момент, который говорит, что внутреннее окружение ссылается на внешнее окружение, и это внешнее окружение может иметь собственное внешнее окружение. В результате окружение может быть внешним окружением для более чем одного внутреннего окружения. Глобальное окружение является единственным лексическим окружением, которое не имеет внешнего окружения. Это сложно описать словами, поэтому давайте использовать метафоры и представим лексическое окружение как слои лука: глобальная среда — внешний слой луковицы, где каждый последующий слой находится ниже.
Так выглядит окружение в псевдокоде:
LexicalEnvironment = < EnvironmentRecord: < // Здесь идёт привязка идентификатора >, // Ссылка на внешнее окружение outer: < >>;
- Новое лексическое окружение, которое создавалось каждый раз при разборе кода — каждый раз, когда вызываются внешние вложенные функции, создаётся новое лексическое окружение. Это важно, и мы вернёмся к этому моменту в конце. Примечание: функции — не единственный способ создать лексическое окружение. Другие типы содержат в себе оператор блока —block statementили условный оператор —catch clause__. Для простоты, я сосредоточусь на окружении созданной нами функции на протяжении всего поста.
Каждый контекст исполнения имеет собственное лексическое окружение. Это лексическое окружение содержит переменные и связанные с ними значения, а также имеет ссылку на него во внешнем окружении. Лексическое окружение может быть глобальным окружением, модульным окружением, которое содержит привязку для объявлений модуля на высшем уровне, или окружением функций, где окружение создаётся за счёт вызова функции.
Цепочки областей видимости
Исходя из приведённого выше определения, мы знаем, что окружение имеет доступ к окружению своего родителя, его родительское окружение имеет доступ к своему родительскому окружению и так далее. Этот набор идентификаторов, к которому каждое окружение имеет доступ, называется область видимости — scope. Мы можем вложить их в иерархические цепочки окружения, известные как цепочки областей видимости.
Давайте рассмотрим эту структуру вложенности:
var x = 10; function foo() < var y = 20; // свободная переменная function bar() < var z = 15; // свободная переменная return x + y + z; >return bar; >
Как вы можете видеть, bar вложен в foo. Чтобы всё это представить посмотрите на диаграмму ниже:

Мы вернёмся позже к этому примеру.
Эта цепочка области видимости или цепочка окружения связанная с функцией сохраняется в объекте функции в момент создания. Другими словами, она определяется статично по местоположению в исходном коде. Это называют лексической областью видимости.
Давайте быстренько разберём разницу между динамической областью видимости и статической областью видимости. Это поможет нам разобраться, почему статическая область видимости или лексическая область видимости необходима для использования замыкания.
Идём в обход: динамическая область видимости против статической области видимости
У динамических языков программирования существует стековая архитектура — stack-based implementations, локальные переменные и функции хранятся в стеке. Поэтому, во время выполнения стека, программа определяет какую переменную вы имеете в виду. С другой стороны, статическая область видимости — это когда переменные ссылаются на контекст и фиксируются на момент создания. Другими словами, структура исходного кода программы определяет к каким переменным вы обращаетесь.
Вы могли задаваться вопросом, как различаются динамическая и статическая области видимости. Вот два примера, которые помогут это продемонстрировать:
Пример 1
var x = 10; function foo() < var y = x + 5; return y; >function bar() < var x = 2; return foo(); >function main() < foo(); // Статическая область: 15; Динамическая область: 15 bar(); // Статическая область: 15; Динамическая область: 7 return 0; >
Как мы видим из примера выше, статическая и динамическая область видимости возвращают разные значения при вызове функции bar.
В статической области видимости возврат значения bar зависит от значения x. Это происходит из-за того, что статическая и лексическая структура исходного кода приводит x и к 10, и к 15.
Динамическая область видимости даёт нам стек определённых переменных, которые отслеживаются во время выполнения. Поэтому x, которую мы используем, зависит от того, что находится в её области видимости и как она была динамично определена во время выполнения. Выполнение функции bar выталкивает x = 2 на верхушку стека, заставляя foo вернуть 7.
Пример 2
var myVar = 100; function foo() < console.log(myVar); >foo(); // Статическая область: 100; Динамическая область: 100 (function () < var myVar = 50; foo(); // Статическая область: 100; Динамическая область: 50 >)(); // Функция высшего порядка (function (arg) < var myVar = 1500; arg(); // Статическая область: 100; Динамическая область: 1500 >)(foo);
Аналогично и в динамической области видимости. Переменная myVar решает, какое использовать значение myVar в зависимости от того, где была вызвана функция. Статическая область видимости приводит myVar к переменной, которая была сохранена в рамках двух немедленно вызываемых функций при их создании.
Как вы можете заметить, динамическая область видимости часто создаёт некоторую двусмысленность. Она не даёт точно понять, какая свободная переменная будет передана.
Замыкания
Вы можете подумать, что всё, о чём мы говорим, совершенно не касается нашей темы, но на самом деле мы разобрали всё, что поможет нам понять замыкания:
Каждая функция имеет контекст выполнения, который состоит из окружения и передаёт смысл в переменные этой функции и ссылку на окружение своего родителя. Эта ссылка делает все переменные в родительской области доступными для всех внутренних функций, независимо от того, вызывается ли внутренняя функция (или функции) вне или внутри области видимости, в которой они были созданы.
Кажется, как будто функция «запоминает» это окружение, поскольку функция буквально имеет ссылку к области видимости и переменным, определённым в этой среде.
Возвратимся к примеру вложенной структуры:
var x = 10; function foo() < var y = 20; function bar() < var z = 15; return x + y + z; >return bar; > var test = foo(); test(); // 45
Основываясь на нашем понимании того, как работает окружение, мы можем сказать, что определение окружения для вышеупомянутого примера выглядит примерно так в псевдокоде:
GlobalEnvironment = < EnvironmentRecord: < // встроенные идентификаторы Array: '', Object: '', // и т.д. // пользовательские идентификаторы x: 10 >, outer: null >; fooEnvironment = < EnvironmentRecord: < y: 20, bar: '' > outer: GlobalEnvironment >; barEnvironment = < EnvironmentRecord: < z: 15 >outer: fooEnvironment >;
Когда мы вызываем функцию test, мы получаем 45, и она возвращает значение из вызова функции bar (потому что foo возвращает bar). bar имеет доступ к свободной переменной y даже после того, как функция foo вернётся, так как bar имеет ссылку на y через его внешнее окружение, которое является окружением foo! bar так же имеет доступ к глобальной переменной x потому, что у окружения foo есть доступ к глобальному окружению. Это называют «поиск цепочки области видимости».
Подведём итог обсуждения динамической области видимости против статической: для замыканий, которые будут выполняться, нельзя использовать динамическую область с помощью динамического стека. Это сохранит наши переменные. Причина такого поведения кроется в том, что когда функция возвращается, переменные будут удалены из стека и больше не будут доступны, а это противоречит нашему определению замыкания. Это происходит из-за того, что замкнутость данных в родительском контексте сохраняется в так называемой «куче», и это позволяет сохранять данные после вызова функции, делая их возможными для возврата, то есть даже после того, как контекст выполнения извлекается из стека выполнения вызова.
Теперь, когда мы понимаем внутренности на абстрактном уровне, давайте рассмотрим ещё пару примеров:
Пример 1
Вот типичное заблуждение: в цикле for мы пробуем связать переменную счётчика с какой-либо функцией.
var result = []; for (var i = 0; i < 5; i++) < result[i] = function () < console.log(i); >; > result[0](); // 5, ожидалось 0 result[1](); // 5, ожидалось 1 result[2](); // 5, ожидалось 2 result[3](); // 5, ожидалось 3 result[4](); // 5, ожидалось 4
На основе всего что было ранее, мы можем с лёгкостью найти ошибку здесь. Абстрактно говоря, вот так выглядит окружение, во время выхода из цикла for:
environment: < EnvironmentRecord: < result: [. ], i: 5 >, outer: null, >
Было бы неверно предполагать, что область видимости отличается для всех пяти функций в результирующем массиве. Вместо того, что происходит по факту, окружение или контекст/область видимости является тем же самым для всех пяти функций в пределах результирующего массива. Поэтому каждый раз, когда переменная i увеличивается, обновляется область видимости, а она является общей для всех функций. Из-за этого любая из пяти функций, пытающихся получить доступ к i, возвращает 5, i равна 5, когда цикл завершается.
У нас есть только один способ исправить это — создать дополнительный вызываемый контекст для каждой функции, чтобы у них появился собственный контекст/область видимости.
var result = []; for (var i = 0; i < 5; i++) < result[i] = (function inner(x) < // дополнительный вызываемый контекст return function() < console.log(x); >>)(i); > result[0](); // 0, ожидалось 0 result[1](); // 1, ожидалось 1 result[2](); // 2, ожидалось 2 result[3](); // 3, ожидалось 3 result[4](); // 4, ожидалось 4
Ура, мы исправили это.
Есть ещё одно решение, в котором мы используем let вместо var, let находится в операторе блока и поэтому новая привязка идентификатора замыкания создаётся для каждой итерации в цикле for.
var result = []; for (let i = 0; i < 5; i++) < result[i] = function () < console.log(i); >; > result[0](); // 0, ожидалось 0 result[1](); // 1, ожидалось 1 result[2](); // 2, ожидалось 2 result[3](); // 3, ожидалось 3 result[4](); // 4, ожидалось 4
Пример 2
В этом примере мы покажем как каждый вызов в функции создаёт новое отдельное замыкание:
function iCantThinkOfAName(num, obj) < // Это массив переменных, вместе с 2 параметрами, передаваемых // "захваченными" в замкнутую функцию '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); // замыкание #1 var bar = iCantThinkOfAName(6, referenceObject); // замыкание #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’s 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: < // встроенные идентификаторы Array: '', Object: '', // и т.д. // пользовательские идентификаторы 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, >;
Это происходит из-за того, что наши функции add и substract ссылаются на среду 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, делая невозможным доступ к password снаружи.
Tl; dr
- Контекст выполнения — это абстрактный контекст, использовавшийся в спецификации ECMAScript для отслеживания времени выполнения кода. В любое время может быть только один контекст выполнения, который выполняет код.
- Каждый контекст исполнения имеет лексическое окружение. Оно содержит связи идентификаторов, то есть переменные и их значения и имеет ссылку на него во внешнем окружении.
- Набор идентификаторов, к которым у каждого окружения есть доступ, называют «область видимости». Мы можем вложить эти области в иерархическую цепь окружения, известной как «цепочки области видимости».
- Каждая функция имеет контекст выполнения, который включает в себя лексическое окружение. Это придаёт смысл переменным в пределах этой функции и ссылку на родительское окружение. И это означает, что функции «запоминают» окружение или область видимости, так как они буквально ссылаются на это окружение. Это и есть замыкание.
- Замыкания создаются каждый раз при вызове внешней функции. Другими словами, внутренняя функция не будет возвращена для замыкания, в котором была создана.
- Область видимости замыканий в JavaScript лексическая, её смысл определяется статично в зависимости от нахождения в исходном коде.
- Есть множество практических случаев использования замыканий. Один из важных случаев использования — это сохранение приватных ссылок к переменным во внешней среде.
Замыкающая ремарка
Я надеюсь этот пост был полезным и дал представление о том, как замыкания реализованы в JavaScript. Как вы видите, понимание внутреннего устройства замыканий и особенностей их работы делают их поиск проще. Теперь у вас будет меньше головной боли при отладке.
Дополнительная литература
Для краткости я опустила несколько тем, которые могут быть интересны для некоторых читателей. Вот несколько ссылок, которыми я хотела бы поделиться:
- Какие среды переменных бывают в контексте выполнения? Аксель Раушмайер сделал феноменальную работу, объясняющую эту тему, и я просто оставлю ссылку на его пост.
- Что является различными типами записи окружения?Читайте спецификацию здесь.
- Отличная статья на MDN про замыканияздесь.
Есть что-то ещё? Предлагайте.
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.
