Я никогда не понимал замыкания в JavaScript. Часть вторая
Nuances of programmingПеревод статьи Olivier De Meulder: I never understood JavaScript closures
Предыдущие части: Часть 1
Наконец, замыкания
Взгляните на следующий код и попробуйте выяснить что произойдёт.
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)
Теперь, используя знания, полученные из примеров в первой части статьи, давайте разберём как это будет работать.
- Строки 1-8. Мы создаём новую переменную
createCounter
в глобальной области выполнения и присваиваем ей описание функции. - Строка 9. Мы объявляем новую переменную
increment
в глобальной области выполнения. - Снова строка 9. Нам нужно вызвать функцию
createCounter
и присвоить возвращённое ей значение переменнойincrement
. - Строки 1-8. Вызываем функцию. Создаём новую локальную область выполнения.
- Строка 2. В локальной области выполнения объявляем новую переменную
counter
. Число0
присваиваетсяcounter
. - Строки 3-6. Объявляем новую переменную
myFunction
. Эта переменная объявлена в локальной области выполнения. Пока что контентом этой переменной является описание другой функции. Эта функция описана в строках 4 и 5. - Строка 7. Возвращаем содержимое переменной
myFunction
. Локальная область выполнения удалена.myFunction
иcounter
больше не существуют. Управление возвращено вызвавшей области. - Строка 9. В вызвавшей области, глобальной области выполнения, значение, возвращаемое функцией
createCounter
присвоено переменнойincrement
. Переменнаяincrement
теперь содержит определение функции. Определение функции, которое было возвращено изcreateCounter
. Она больше не называетсяmyFunction,
но имеет то же определение. В глобальной области она называетсяincrement
. - Строка 10. Объявление новой переменной
c1
. - Строка 10 (продолжение). Смотрим на переменную
increment
. Это функция. Вызываем её. Она содержит определение функции, которое было возвращено ранее и было описано в строках 4-5. - Создаём новую область выполнения. Без параметров. Начинаем выполнение функции.
- Строка 4.
counter = counter + 1
. Ищем переменнуюcounter
в локальной области выполнения. Мы только что создали эту область выполнения и ещё не создавали никаких переменных. Давайте посмотрим в глобальной области выполнения. Никакой переменной с названиемcounter
не найдено. JavaScript будет обрабатывать это какcounter = undefined + 1
, объявив при этом новую переменную с названиемcounter
, и присвоив ей значение1
, так какundefined
это что-то вроде0
. - Строка 5. Мы возвращаем значение переменной
counter
, а именно число1
. Мы уничтожаем локальную область выполнения вместе с переменнойcounter
. - Возвращаемся к строке 10. Возвращённое значение (
1
) присвоеноc1
. - Строка 11. Мы повторяем шаги 10-14,
c2
также получает значение1
. - Строка 12. Мы повторяем шаги 10-14,
c3
также получает значение1
. - Строка 13. Мы выводим в консоль значения переменных
c1
,c2
иc3
.
Попробуйте сами и посмотрите, что произойдёт. Вы заметите, что в консоль не попадают значения 1
, 1
и 1
, как вы можете ожидать после объяснений выше. Вместо этого в логи попадают 1
, 2
и 3
. Как так вышло?
Каким-то образом инкремент-функция запоминает значение counter
. Как это работает?
Разве counter
часть глобальной области выполнения? Если вы попробуете написать console.log(counter)
, то получите значение undefined
. Значит этот вариант неверен.
Может быть, когда вы вызываете increment
, то каким-то образом оно возвращается к той функции, где было создано (createCounter
)? Как это вообще может работать? Переменная increment
содержит описание функции, а не место, в котором она описана. А значит это тоже неверно.
А значит должен быть должен быть иной механизм. Замыкание. Мы наконец нашли его, потерянный кусочек пазла.
Вот как это работает. Когда вы объявляете новую функцию и присваиваете её переменной, то в этой переменной вы храните не только определение функции, но и её замыкание. Замыкание содержит все переменные, которые находятся в области видимости во время создания функции. Это аналогично рюкзаку. Определение функции идёт вместе с маленьким рюкзаком. А хранятся в нём все переменные, которые были в поле видимости в то время, когда функция создавалась.
Это значит, что все наши объяснения выше были неверны. Давайте попробуем ещё раз, но теперь более верно.
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)
- Строки 1-8. Мы создаём новую переменную
createCounter
в глобальной области выполнения и присваиваем ей описание функции. Так же, как и раньше. - Строка 9. Мы объявляем новую переменную
increment
в глобальной области выполнения. Так же, как и раньше. - Снова строка 9. Нам нужно вызвать функцию
createCounter
и присвоить возвращённое ей значение переменнойincrement
. Так же, как и раньше. - Строки 1-8. Вызываем функцию. Создаём новую локальную область выполнения. Так же, как и раньше.
- Строка 2. В локальной области выполнения объявляем новую переменную
counter
. Число0
присваиваетсяcounter
. Так же, как и раньше. - Строки 3-6. Объявляем новую переменную
myFunction
. Эта переменная объявлена в локальной области выполнения. Пока что контентом этой переменной является описание другой функции. Эта функция описана в строках 4 и 5. Но также мы создаём замыкание, которое является частью функции. Замыкание хранит переменные из своей области видимости. В нашем случае это переменная КАУНТЕР (значение которой 0). - Строка 7. Возвращаем содержимое переменной
myFunction
. Локальная область выполнения удалена.myFunction
иcounter
больше не существуют. Управление возвращено вызвавшей области. Таким образом мы возвращаем описание функции и её замыкание, рюкзак с переменными, которые были в области видимости во время её создания. - Строка 9. В вызвавшей области, глобальной области выполнения, значение, возвращаемое функцией
createCounter
присвоено переменнойincrement
. Переменнаяincrement
теперь содержит определение функции (и замыкание). Определение функции, которое было возвращено изcreateCounter
. Она больше не называетсяmyFunction,
но имеет то же определение. В глобальной области она называетсяincrement
. - Строка 10. Объявление новой переменной
c1
. - Строка 10 (продолжение). Смотрим на переменную
increment
. Это функция. Вызываем её. Она содержит определение функции, которое было возвращено ранее и было описано в строках 4-5 (в которой также хранится и рюкзак с переменными). - Создаём новую область выполнения. Без параметров. Начинаем выполнение функции.
- Строка 4.
counter = counter + 1
. Ищем переменнуюcounter
. Перед тем, как поискать в локальной или глобальной области выполнения, давайте посмотрим в нашем рюкзаке. Проверяем замыкание. Оказывается, замыкание содержит переменнуюcounter
со значением0
. После выражения на строке 4 её значение установлено в1
. И она снова хранится в рюкзаке. Теперь замыкание хранит переменнуюcounter
со значением 1. - Строка 5. Мы возвращаем значение переменной
counter
, а именно число1
. Мы уничтожаем локальную область выполнения. - Возвращаемся к строке 10. Возвращённое значение (
1
) присвоеноc1
. - Строка 11. Мы повторяем шаги 10-14. В этот раз, когда мы посмотрим в нашем замыкании, то увидим, что переменная
counter
хранит значение 1. Оно было задано в 12-ом шаге, или на 4-ой строке программы. Это значение было увеличено и сохранено как2
в замыкании инкремент-функции. Таким образомc2
присваивается2
. - Строка 12. Мы повторяем шаги 10-14,
c3
получает значение3
. - Строка 13. Мы выводим в консоль значения переменных
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
.
Заключение
Способ, с помощью которого я навсегда запомнил замыкания - это сравнение их с рюкзаком. Когда функция создана и передаётся куда-либо, или возвращается из другой функции, то она носит с собой рюкзак. А в этом рюкзаке хранятся все переменные, которые были в области видимости во время создания этой функции.
Статью перевёл Дмитрий Хирш