Что такое замыкание и как оно работает? Очередной достаточно частый вопрос на собеседованиях. Я всегда думал что я знаю что ответить на этот вопрос. И всегда отвечал что типа такого… Замыкание это когда функция низшего порядка получается доступе к переменным функции высшего порядка. Но знать приблизительное определение и реально разбираться в замыкание не совсем одно и тоже. Давайте рассмотрим простейший пример:
function createCounter() { let counter = 0 const myFunction = function() { counter = counter + 1 return counter } return myFunction } const increment = createCounter() const c1 = increment() const c2 = increment() const c3 = increment() console.log('example increment', c1, c2, c3)
Как по вашему что должно быть выведено в консоль? Если вы точно знаете ответ, то можете пропустить эту статью. А если думаете что 1,1,1 или если у вас возникло какое либо сомнение, рекомендую продолжить чтение далее….
Статья получилась большой, с длинными последовательностями описания маленьких примеров. Но если вас это не смутит и вы пройдетесь по этим описаниям, в награду получите более полное понимание того как работает замыкание.
Перед тем как мы продолжим, хотел оговорить некоторые важные понятия, которые нам понадобятся дальше. Одним из них является контекст исполнения.
Это статья хорошо объясняет что такое Контекст исполнения (Execution Context). Основные понятия из статьи:
Когда код выполняется, среда, в которой он выполняется, очень важна и определяется как то так:
Глобальный код (Global code ) — Среда по умолчанию, где код выполняется в первый раз.
Код функции (Function code) — всякий раз, когда поток выполнения входит в тело функции.
(…), давайте думать о термине контекст исполнения как о среде/области видимости (scope), в которой выполняется текущий код.
Другими словами, когда запускается программа, ее код начинается исполнятся в глобальном контексте. Как правило некоторое количество переменных объявляются в глобальном контексте выполнения. Мы называем их глобальными переменными. А что происходит когда программа вызывает функцию? Вот список шагов:
Что происходит когда функция заканчивает выполнение? Выполнение заканчивается когда встречается оператор return
или соответствующая закрывающая скобка }
:
return
, возвращается undefined
.Прежде чем мы перейдем к замыканиям, давайте рассмотрим следующий фрагмент кода. Он покажется очень простым, любой, кто читает эту статью, наверняка точно поймет, что он делает.
1: let a = 3 2: function addTwo(x) { 3: let ret = x + 2 4: return ret 5: } 6: let b = addTwo(a) 7: console.log(b)
Чтобы понять, как на самом деле работает движок JavaScript, давайте разберем этот пример подробно.
a
в глобальном контексте выполнения и ей присваивается значение 3addTwo
в глобальном контексте выполнения. И что ей назначается? Определение функции. Все, что находится между двумя скобками, { }
присваивается addTwo
. Код внутри функции на данный момент не выполняется, он просто сохраняется в переменной для будущего использования. b
в глобальном контексте. Как только переменная объявлена, она имеет значение undefined
.(…)
, это сигнал о том, что это вызов функции. Забегая вперед, заметим что каждая функция возвращает что-либо (значение, объект или undefined
). Все, что возвращается из функции, будет присвоено переменной b
. addTwo
. Что бы ее запустить JavaScript нужно найти переменную с именем addTwo
в глобальном контексте. Он ее найдет, так как мы ее объявили в строке 2 (или строки 2–5). Обратите внимание, что переменная a
передается в качестве аргумента функции. JavaScript так же будет искать переменную a
в глобальном контекста, и после того как, найдет ее, передаст число 3 в качестве аргумента функции. Теперь все готово что бы выполнить функцию. ret
. Это не совсем верный ответ. Правильный ответ: сначала объявляются параметры функции. То есть в нашем случае объявляется новая переменная x
в локальном контексте. И поскольку значение 3 было передано в качестве аргумента, переменной x
присваивается значение 3. ret
объявляется в локальном контексте. Ее значение устанавливается как undefined. (строка 3)x
. JavaScript начинает искать переменную x
. В начале она будет искаться в локальном контексте. И сразу же там будет найдена со значение 3. А вторым операндом будет число 2. Результат сложения (5) присваивается переменной ret.addTwo
была вызвана из глобального контекста. Это было очень длинное объяснение очень простой программы, и мы еще даже не коснулись замыканий. Мы доберемся до них чуть позже, я обещаю. Но сначала нам нужно коснуться еще одного или двух пояснений.
Далее я бы хотел коснуться некоторых аспектов лексического контекста. Посмотрите на следующий пример.
1: let val1 = 2 2: function multiplyThis(n) { 3: let ret = n * val1 4: return ret 5: } 6: let multiplied = multiplyThis(6) 7: console.log('example of scope:', multiplied)
Идея в том, что у нас есть переменные в локальном контексте и переменные в глобальном контексте. Сложность для JavaScript состоит в поиске переменных. Если JavaScript не может найти переменную в локальном контексте выполнения, она будет искать ее в вызывающем контексте, если она не найдет ее там далее она будет искать переменную в глобальном контексте. (И если JavaScript не найдет ее там, переменная станет undefined
). Рассмотрим пример выше. Если вы понимаете, как работает поиск, вы можете пропустить рассмотрения примера.
val1
в глобальном контексте и назначение ей значение 2
.multiplyThis
и назначение ей определение функции.multiplied
в глобальном контексте.multiplyThis
из глобального контекста и выполнение функции. Передача значения 6
в качестве аргумента.n
и ей назначается значение 6.ret
.n
и val1
. Переменная n
берется из локального контекста. Мы объявили ее в строке 6. Ее значение 6
. Ищется переменная val1
в локальном контексте. В локальном контексте ее нет. Далее ищется в вызываемом контексте. В данном случае вызываемый контекст является глобальным. И там она находится. Она была определена в строке 1. Ее значение равно 2
.ret
. 6 * 2 = 12. Значение ret
в данный момент 12
.ret
. Локальный контекст уничтожается вместе с переменными ret
и n
. Переменная val1
не уничтожается, так как она находится в глобальном контексте.multiplied
назначается значение 12
.multiplied
отображается в консоле.Таким образом, в этом примере мы продемонстрировали, что функция имеет доступ к переменным, которые определены в контексте вызова. Формальное название этого явления — лексическая область видимости (lexical scope).
В первом примере функция addTwo
возвращает номер. Мы знаем что функция может вернуть что угодно. Давайте рассмотрим пример когда функция возвращает функцию.
1: let val = 7 2: function createAdder() { 3: function addNumbers(a, b) { 4: let ret = a + b 5: return ret 6: } 7: return addNumbers 8: } 9: let adder = createAdder() 10: let sum = adder(val, 8) 11: console.log('example of function returning a function: ', sum)
val
в глобальном контексте и ей назначили значение 7
.createAdder
в глобальном контексте и ей назначили определение функции. Строки с 3 по 7 определяют тело функции. Как и ранее на этом шаге сама функция не запускается. Только сохраняется ее определение в переменной createAdder
.adder
, в глобальном контексте. Временно ее значение в текущий момент undefined
.()
; мы запускаем функцию на выполнение. В глобальном контексте ищется переменная createAdder
. Она была определена в строке 2. Далее запускается функцию.addNumbers
. Это важно! addNumbers
существует только в локальном контексте. Далее определение функции сохраняется в локальной переменной addNumbers
.addNumbers
. В ней находится определение функции (строки с 4 по 5). Далее удаляется локальный контекст из стека вызовов.addNumbers
более не существует. Однако определение функции все еще существует, он возвращается из функции и назначается переменной adder
; это переменная было создана в строке 3.sum
в глобальном контексте. И ей временно назначается значение undefined
.adder
. Движок ищет и находит ее в глобальном контексте. У этой функции есть два параметра.val
определена в строке 1, ее значение 7
, вторая переменная просто значение 8
.a
и b
. У них значения 7
и 8
соотвественно.ret
. a
и b
. Результат сложения (15
) назначается переменной ret
.ret
возвращается из функции. Локальный контекст уничтожается и удаляется из стека выполнения, переменные a
, b
и ret
более не существует.sum
определенной в строке 9.sum
выводится в консоле.Как и ожидалось, в консоли отобразиться 15. Этим примером я пытаюсь проиллюстрировать несколько моментов. Во-первых, определение функции может быть сохранено в переменной, определение функции невидимо для программы, пока функция не будет вызвано. Во-вторых, каждый раз, когда вызывается функция, создается (временно) локальный контекст выполнения. После завершения функции этот контекст исчезает. Функция завершает свое выполнение, когда в коде встречается return
или соответствующую закрывающую скобку}.
Посмотрим на следующий пример и попытайтесь понять, что здесь происходит.
1: function createCounter() { 2: let counter = 0 3: const myFunction = function() { 4: counter = counter + 1 5: return counter 6: } 7: return myFunction 8: } 9: const increment = createCounter() 10: const c1 = increment() 11: const c2 = increment() 12: const c3 = increment() 13: console.log('example increment', c1, c2, c3)
Теперь, когда мы познакомились с предыдущими двумя примерами, давайте рассмотрим выполнение этого.
createCounter
в глобальном контексте и ей назначается определение функции.increment
.createCounter
и ее возвращаемое значение назначается переменной increment
.counter
. Ей назначается значение 0
.myFunction
. Значение переменной объявление еще одной функции (строки 4 и 5).myFunction
. Локальный контекст удаляется. Переменные myFunction
и counter
больше не существуют. Контроль передается в вызывающий контекст.createCounter
назначается increment
. Сейчас в этой переменной определение функции. И оно больше не помечено именем myFunction
, внутри глобального контекста оно названо increment
.c1
).increment
, после того как оно будет найдено и определено что это функция, она будет запущена.counter = counter + 1
. Начинается поиск counter
в локальном контексте. Мы только что его создали и в нем пока еще ничего нет. Далее ищется в глобальном контексте. Но там тоже нет переменной counter
. Javascript будет рассматривать это выражение как counter = undefined + 1
, далее объявит новую локальную переменную counter
и назначит ей значение 1
, так как undefined
воспринимается как 0
.counter
, а точнее ее значение 1
. Уничтожается локальный контекст и переменная counter
.1
) назначается переменной c1
.c2
получает 1
.c3
получает 1
.c1
, c2
и c3
.Попробуйте сами запустить этот пример и посмотрите, что получится. Вы заметите, что на самом деле выводится не 1
, 1
, и 1
как мы только что рассмотрели выше. Вместо этого выводится 1
, 2
и 3
. Так что здесь происходит?
Каким-то образом функция increment запоминает значение счетчика.
Переменная counter
находится в глобальном контексте ? Попытайтесь выполнить console.log(counter)
и вы получите counter is not defined. Так что нет, не находится.
Может быть, когда мы вызываем increment
, каким-то образом он возвращается к функции, в которой он был создан (createCounter
)? Как это вообще работает? Переменная increment
содержит определение функции, а не то откуда она вызвалась.
Так что должен быть другой механизм. И он называется Замыкание Мы наконец дошли до этого!
Вот как это работает. Всякий раз, когда вы объявляете новую функцию и присваиваете ее переменной, вы сохраняете определение функции, а также замыкание. Замыкание содержит все переменные, которые находятся в области действия на момент создания функции. Это аналог рюкзака. Определение функции идет как бы с «небольшим рюкзаком». И в своем «рюкзаке» оно хранит все переменные, которые находились в области действия (scope) на момент создания определения функции.
Итак, наше объяснение выше было неверным, давайте попробуем еще раз, но на этот раз правильно.
1: function createCounter() { 2: let counter = 0 3: const myFunction = function() { 4: counter = counter + 1 5: return counter 6: } 7: return myFunction 8: } 9: const increment = createCounter() 10: const c1 = increment() 11: const c2 = increment() 12: const c3 = increment() 13: console.log('example increment', c1, c2, c3)
createCounter
и ей назначается определение функции.increment
.createCounter
и ее возвращаемое значение передается переменной increment
.counter
. Ее назначается значение 0
.myFunction
. Переменная содержит объявление еще одной функции строки 4 и 5. Сейчас так же создается замыкание и включается в определение функции. Замыкание содержит переменные, находящиеся в области видимости, в данном случае переменная counter
(со значением 0
). myFunction
. Локальный контекст удаляется. Переменные myFunction
и counter
перестают существовать. Контроль передается в вызывающий контекст. Таким образом, мы возвращаем определение функции и ее замыкание, «рюкзак» с переменными, которые находились в области видимости в момент ее создания.createCounter
передается в переменную increment
. Переменная increment сейчас содержит определение функции (и замыкание). c1
.increment
, после того как она будет найдена будет определенно ее содержимое. Она содержит определение функции в строках 4–5. (а так же содержит «рюкзак» с переменными)counter = counter + 1
. Ищется переменная counter
. Прежде чем начнется поиск в локальном и глобальном контексте давай те посмотрим что в нашем «рюкзаке». Точнее посмотрим что в замыкании. А вот замыкание содержит переменную counter
, со значением 0
. Поэтому выражению в строке 4, назначается значение 1
. И это значение сохраняется в «рюкзаке». Замыкание сейчас содержит переменную counter
со значением 1
.counter
, а точнее ее значение 1
. Далее уничтожается локальный контекст.1
) назначается переменной c1
.counter
имеет значение 1. Она была установлена в строке 12 или строке 4 программы. Его значение увеличивается и сохраняется со значением 2
при закрытии функции increment. И переменная c2
получает значение 2
. c3
получает значение 3
.c1
, c2
и c3
.Итак, теперь мы понимаем, как это работает. Необходимо помнить, что когда функция объявляется, она содержит определение функции и замыкание. Так что такое замыкание?
Замыкание — это коллекция всех переменных в области видимости во время создания функции.
Вы можете спросить, любая функция имеет замыкание, даже если она создается в глобальной области видимости? Ответ — да. Функции, созданные в глобальной области видимости, так же создают замыкание. Но так как эти функции были созданы в глобальной области, они имеют доступ ко всем переменным в глобальной области. И концепция замыкания для них не очень актуальна.
Концепция замыканий становится актуальной, когда функция возвращает функцию. Возвращенная функция имеет доступ к переменным, которых нет в глобальной области видимости, но они существуют только в ее замыкание.
Иногда замыкания появляются, когда вы даже не замечаете этого. Возможно, вы видели похожий пример:
let c = 4 const addX = x => n => n + x const addThree = addX(3) let d = addThree(c) console.log('example partial application', d)
В случае, если стрелочные функции сбивает вас с толку, напишем эквивалент.
let c = 4 function addX(x) { return function(n) { return n + x } } const addThree = addX(3) let d = addThree(c) console.log('example partial application', d)
Мы объявляем функцию сумматора addX
, которая принимает один параметр (x
) и возвращает другую функцию.
Возвращаемая функция также принимает один параметр и добавляет его к переменной x
.
Переменная x
является частью замыкания. Когда переменная addThree
объявляется в локальном контексте, ей назначается определение функции и замыкание. Замыкание содержит переменную x
.
Так что теперь, когда addThree
вызывается и выполняется, она имеет доступ к переменной x
из своего замыкания и переменной n
, которая была передана в качестве аргумента.
В этом примере в консоль будет выведено число 7.
Аналогия о которой я всегда буду помнить в случае замыканий — это аналогия с рюкзаком. Когда функция создается и передается или возвращается из другой функции, она несет с собой этот самый «рюкзак». И в этом «рюкзаке» находятся все переменные, которые были в области видимости, когда функция была объявлена.
Статья написана на основе I never understood JavaScript closures
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
могу сказать одно. counter никогда не будет undefined в данном примере замыкания.
Вы правы. Спасибо. Поправил перевод.
Для меня до сих пор непонятно одно - почему counter с каждым вызовом функции увеличивается. Почему let counter = 0 не сбрасывает при каждом вызове функции в 0?
То-есть этот "рюкзак" с переменными постоянно где-то храниться и изменяется?
еще интересно, что рюкзак-то общий на все вложенные, вот так это видно:
function createCounter() {
var numberOfCalls = 0;
window.a1=function() {
return ++numberOfCalls;
}
window.a2=function() {
return --numberOfCalls;
}
}
createCounter();
alert (a1()); //1
alert (a1()); //2
alert (a1()); //3
alert (a2()); //2
alert (a2()); //1