Что такое замыкание в программировании
Замыкания в JavaScript для начинающих
Замыкания — это одна из фундаментальных концепций JavaScript, вызывающая сложности у многих новичков, знать и понимать которую должен каждый JS-программист. Хорошо разобравшись с замыканиями, вы сможете писать более качественный, эффективный и чистый код. А это, в свою очередь, будет способствовать вашему профессиональному росту.
Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.
Что такое замыкание?
Замыкание — это функция, у которой есть доступ к области видимости, сформированной внешней по отношению к ней функции даже после того, как эта внешняя функция завершила работу. Это значит, что в замыкании могут храниться переменные, объявленные во внешней функции и переданные ей аргументы. Прежде чем мы перейдём, собственно, к замыканиям, разберёмся с понятием «лексическое окружение».
Что такое лексическое окружение?
Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:
Здесь у функции inner() есть доступ к переменным, объявленным в её собственной области видимости, в области видимости функции outer() и в глобальной области видимости. Функция outer() имеет доступ к переменным, объявленным в её собственной области видимости и в глобальной области видимости.
Цепочка областей видимости вышеприведённого кода будет выглядеть так:
Практические примеры замыканий
Рассмотрим, прежде чем разбирать тонкости внутреннего устройства замыканий, несколько практических примеров.
▍Пример №1
▍Пример №2
Как работают замыкания?
До сих пор мы говорили о том, что такое замыкания, и рассматривали практические примеры. Теперь поговорим о внутренних механизмах JavaScript, обеспечивающих их работу.
Для того чтобы понять замыкания, нам нужно разобраться с двумя важнейшими концепциями JavaScript. Это — контекст выполнения (Execution Context) и лексическое окружение (Lexical Environment).
▍Контекст выполнения
Контекст выполнения — это абстрактное окружение, в котором вычисляется и выполняется JavaScript-код. Когда выполняется глобальный код, это происходит внутри глобального контекста выполнения. Код функции выполняется внутри контекста выполнения функции.
В некий момент времени может выполняться код лишь в одном контексте выполнения (JavaScript — однопоточный язык программирования). Управление этими процессами ведётся с использованием так называемого стека вызовов (Call Stack).
Стек вызовов — это структура данных, устроенная по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Новые элементы можно помещать только в верхнюю часть стека, и только из неё же элементы можно изымать.
Текущий контекст выполнения всегда будет в верхней части стека, и когда текущая функция завершает работу, её контекст выполнения извлекается из стека и управление передаётся контексту выполнения, который был расположен ниже контекста этой функции в стеке вызовов.
Рассмотрим следующий пример для того, чтобы лучше разобраться в том, что такое контекст выполнения и стек вызовов:
Пример контекста выполнения
Стек вызовов этого кода выглядит так:
▍Лексическое окружение
Каждый раз, когда JS-движок создаёт контекст выполнения для выполнения функции или глобального кода, он создаёт и новое лексическое окружение для хранения переменных, объявляемых в этой функции в процессе её выполнения.
Лексическое окружение — это структура данных, которая хранит сведения о соответствии идентификаторов и переменных. Здесь «идентификатор» — это имя переменной или функции, а «переменная» — это ссылка на объект (сюда входят и функции) или значение примитивного типа.
Лексическое окружение содержит два компонента:
Взглянем на следующий фрагмент кода:
Когда JS-движок создаёт глобальный контекст выполнения для выполнения глобального кода, он создаёт и новое лексическое окружение для хранения переменных и функций, объявленных в глобальной области видимости. В результате лексическое окружение глобальной области видимости будет выглядеть так:
Обратите внимание на то, что когда функция завершит работу, её контекст выполнения извлекается из стека вызовов, но её лексическое окружение может быть удалено из памяти, а может и остаться там. Это зависит от того, существуют ли в других лексических окружениях ссылки на данное лексическое окружение в виде ссылок на внешнее лексическое окружение.
Подробный разбор примеров работы с замыканиями
Теперь, когда мы вооружились знаниями о контексте выполнения и о лексическом окружении, вернёмся к замыканиям и более глубоко проанализируем те же фрагменты кода, которые мы уже рассматривали.
▍Пример №1
Взгляните на данный фрагмент кода:
Её лексическое окружение будет выглядеть так:
Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName() ), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:
В функции displayName() нет переменных, поэтому её запись окружения будет пустой. В процессе выполнения этой функции JS-движок попытается найти переменную name в лексическом окружении функции.
▍Пример №2
Лексическое окружение функции getCounter() будет выглядеть так:
При выполнении этой функции система будет искать переменную counter в её лексическом окружении. В данном случае, опять же, запись окружения функции пуста, поэтому поиск переменной продолжается во внешнем лексическом окружении функции.
В результате лексическое окружение функции getCounter() после первого вызова функции count() будет выглядеть так:
Итоги
В этом материале мы поговорили о том, что такое замыкания, и разобрали глубинные механизмы JavaScript, лежащие в их основе. Замыкания — одна из важнейших фундаментальных концепций JavaScript, её должен понимать каждый JS-разработчик. Понимание замыканий — это одна из ступеней пути к написанию эффективных и качественных приложений.
Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.
Замыкания в JavaScript
Если вы используете JavaScript, но при этом так до конца и не разобрались, что же это за чудная штука такая — замыкания, и зачем она нужна — эта статья для вас.
Как известно, в JavaScript областью видимости локальных переменных (объявляемых словом var) является тело функции, внутри которой они определены.
Если вы объявляете функцию внутри другой функции, первая получает доступ к переменным и аргументам последней:
function outerFn(myArg) <
var myVar;
function innerFn() <
//имеет доступ к myVar и myArg
>
>
При этом, такие переменные продолжают существовать и остаются доступными внутренней функцией даже после того, как внешняя функция, в которой они определены, была исполнена.
Рассмотрим пример — функцию, возвращающую кол-во собственных вызовов:
function createCounter() <
var numberOfCalls = 0;
return function () <
return ++numberOfCalls;
>
>
var fn = createCounter();
fn(); //1
fn(); //2
fn(); //3
В данном примере функция, возвращаемая createCounter, использует переменную numberOfCalls, которая сохраняет нужное значение между ее вызовами (вместо того, чтобы сразу прекратить своё существование с возвратом createCounter).
Именно за эти свойства такие «вложенные» функции в JavaScript называют замыканиями (термином, пришедшим из функциональных языков программирования) — они «замыкают» на себя переменные и аргументы функции, внутри которой определены.
Применение замыканий
var fn = ( function () <
var numberOfCalls = 0;
return function () <
return ++ numberOfCalls;
>
>)();
Такая конструкция позволила нам привязать к функции данные, сохраняющиеся между ее вызовами — это одно из применений замыканий. Иными словами, с помощью них мы можем создавать функции, имеющие своё изменяемое состояние.
Другое хорошее применение замыканий — создание функций, в свою очередь тоже создающих функции — то, что некоторые назвали бы приёмом т.н. метапрограммирования. Например:
var createHelloFunction = function (name) <
return function () <
alert( ‘Hello, ‘ + name);
>
>
var sayHelloHabrahabr = createHelloFunction( ‘Habrahabr’ );
sayHelloHabrahabr(); //alerts «Hello, Habrahabr»
Благодаря замыканию возвращаемая функция «запоминает» параметры, переданные функции создающей, что нам и нужно для подобного рода вещей.
Похожая ситуация возникает, когда мы внутреннюю функцию не возвращаем, а вешаем на какое-либо событие — поскольку событие возникает уже после того, как исполнилась функция, замыкание опять же помогает не потерять переданные при создании обработчика данные.
Рассмотрим чуть более сложный пример — метод, привязывающий функцию к определённому контексту (т.е. объекту, на который в ней будет указывать слово this).
В этом примере с помощью замыканий функция, вощвращаемая bind’ом, запоминает в себе начальную функцию и присваиваемый ей контекст.
Следующее, принципиально иное применение замыканий — защита данных (инкапсуляция). Рассмотрим следующую конструкцию:
Очевидно, внутри замыкания мы имеем доступ ко всем внешним данным, но при этом оно имеет и собственные. Благодаря этому мы можем окружать части кода подобной конструкцией с целью закрыть попавшие внутрь локальные переменные от доступа снаружи. (Один из примеров ее использования вы можете увидеть в исходном коде библиотеки jQuery, которая окружает замыканием весь свой код, чтобы не выводить за его пределы нужные только ей переменные).
Есть, правда, одна связанная с таким применением ловушка — внутри замыкания теряется значение слова this за его пределами. Решается она следующим образом:
Рассмотрим еще один приём из той же серии. Повсеместно популяризовали его разработчики фреймворка Yahoo UI, назвав его «Module Pattern» и написав о нём целую статью в официальном блоге.
Пускай у нас есть объект (синглтон), содержащий какие-либо методы и свойства:
var MyModule = ( function () <
var name = ‘Habrahabr’ ;
function sayPreved() <
alert( ‘PREVED ‘ + name.toUpperCase());
>
return <
sayPrevedToHabrahabr: function () <
sayPreved(name);
>
>
>)();
MyModule.sayPrevedToHabrahabr(); //alerts «PREVED Habrahabr»
Напоследок хочу описать распространённую ошибку, которая многих вгоняет в ступор в случае незнания того, как работают замыкания.
Пускай у нас есть массив ссылок, и наша задача — сделать так, чтобы при клике на каждую выводился алертом ее порядковый номер. Первое решение, что приходит в голову, выглядит так:
На деле же оказывается, что при клике на любую ссылку выводится одно и то же число — значение links.length. Почему так происходит? В связи с замыканием объявленная вспомогательная переменная i продолжает существовать, при чём и в тот момент, когда мы кликаем по ссылке. Поскольку к тому времени цикл уже прошёл, i остаётся равным кол-ву ссылок — это значение мы и видим при кликах.
Решается эта проблема следующим образом:
Здесь с помощью еще одного замыкания мы «затеняем» переменную i, создавая ее копию в его локальной области видимости на каждом шаге цикла. Благодаря этому всё теперь работает как задумывалось.
Вот и всё. Эта статья, конечно, не претендует на звание исчерпывающей, но кому-нибудь, надеюсь, всё-таки поможет разобраться. Спасибо за внимание!
Замыкания, функции изнутри
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/closure.
В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций.
Лексическое окружение
Мы будем называть этот объект «лексическое окружение» или просто «объект переменных».
Пример
Посмотрим пример, чтобы лучше понимать, как это работает:
При вызове функции:
До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создаёт пустой объект LexicalEnvironment и заполняет его.
В данном случае туда попадает аргумент name и единственная переменная phrase :
В конце выполнения функции объект с переменными обычно выбрасывается и память очищается. В примерах выше так и происходит. Через некоторое время мы рассмотрим более сложные ситуации, при которых объект с переменными сохраняется и после завершения функции.
Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.
Доступ ко внешним переменным
Из функции мы можем обратиться не только к локальной переменной, но и к внешней:
Это свойство никогда не меняется. Оно всюду следует за функцией, привязывая её, таким образом, к месту своего рождения.
Если переменная не найдена в функции – она будет искаться снаружи.
Именно благодаря этой механике в примере выше alert(userName) выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции.
Всегда текущее значение
Значение переменной из внешней области берётся всегда текущее. Оно может быть уже не то, что было на момент создания функции.
Например, в коде ниже функция sayHi берёт phrase из внешней области:
Это естественно, ведь для доступа к внешней переменной функция по ссылке [[Scope]] обращается во внешний объект переменных и берёт то значение, которое там есть на момент обращения.
Вложенные функции
Внутри функции можно объявлять не только локальные переменные, но и другие функции.
К примеру, вложенная функция может помочь лучше организовать код:
Вложенные функции получают [[Scope]] так же, как и глобальные. В нашем случае:
Заметим, что если переменная не найдена во внешнем объекте переменных, то она ищется в ещё более внешнем (через [[Scope]] внешней функции), то есть, такой пример тоже будет работать:
Возврат функции
Рассмотрим более «продвинутый» вариант, при котором внутри одной функции создаётся другая и возвращается в качестве результата.
В разработке интерфейсов это совершенно стандартный приём, функция затем может назначаться как обработчик действий посетителя.
Здесь мы будем создавать функцию-счётчик, которая считает свои вызовы и возвращает их текущее число.
В примере ниже makeCounter создаёт такую функцию:
Если подробнее описать происходящее:
На этом создание «счётчика» завершено.
Возвращённая из makeCounter() функция counter помнит (через [[Scope]] ) о том, в каком окружении была создана.
Это и используется для хранения текущего значения счётчика.
Далее, когда-нибудь, функция counter будет вызвана. Мы не знаем, когда это произойдёт. Может быть, прямо сейчас, но, вообще говоря, совсем не факт.
Переменную во внешней области видимости можно не только читать, но и изменять.
В примере выше было создано несколько счётчиков. Все они взаимно независимы:
Свойства функции
Функция в JavaScript является объектом, поэтому можно присваивать свойства прямо к ней, вот так:
Свойства функции не стоит путать с переменными и параметрами. Они совершенно никак не связаны. Переменные доступны только внутри функции, они создаются в процессе её выполнения. Это – использование функции «как функции».
А свойство у функции – доступно отовсюду и всегда. Это – использование функции «как объекта».
Если хочется привязать значение к функции, то можно им воспользоваться вместо внешних переменных.
В качестве демонстрации, перепишем пример со счётчиком:
При запуске пример работает также.
Принципиальная разница – во внутренней механике и в том, что свойство функции, в отличие от переменной из замыкания – общедоступно, к нему имеет доступ любой, у кого есть объект функции.
Например, можно взять и поменять счётчик из внешнего кода:
Иногда свойства, привязанные к функции, называют «статическими переменными».
В некоторых языках программирования можно объявлять переменную, которая сохраняет значение между вызовами функции. В JavaScript ближайший аналог – такое вот свойство функции.
Итого: замыкания
Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны.
Таково стандартное определение, которое есть в Wikipedia и большинстве серьёзных источников по программированию. То есть, замыкание – это функция + внешние переменные.
Тем не менее, в JavaScript есть небольшая терминологическая особенность.
Обычно, говоря «замыкание функции», подразумевают не саму эту функцию, а именно внешние переменные.
Иногда говорят «переменная берётся из замыкания». Это означает – из внешнего объекта переменных.
Иногда говорят «Вася молодец, понимает замыкания!». Что это такое – «понимать замыкания», какой смысл обычно вкладывают в эти слова?
«Понимать замыкания» в JavaScript означает понимать следующие вещи:
В следующих главах мы углубим это понимание дополнительными примерами, а также рассмотрим, что происходит с памятью.
Как работают замыкания (под капотом) в JavaScript
Мы в Хекслете используем JavaScript не только для очевидных задач во фронтэнде, но и, например, для реализации браузерной среды разработки (наш опен-сорсный hexlet-ide) на React’е. У нас есть практический курс по JavaScript, и один из уроков там посвящен замыканиям. Это важная тема не столько в рамках JS, сколько в программировании вообще. Мы освещаем ее и в других курсах.
В целом, статей и туториалов про использование замыканий в JS полно, но объяснений как это все работает внутри — мало. Сегодняшний перевод посвящен именно этой теме. Как и почему работают замыкания в JS, когда они создаются и уничтожаются и почему каждая функция в JS — это замыкание.
Я использую замыкания уже достаточно давно. Я научился их использовать, но не до конца понимал как они на самом деле работают, что происходит «под капотом». Что это вообще такое? Википедия не очень помогает. Когда замыкание создается и уничтожается? Как выглядит реализация?
Когда я наконец все выяснил, мне захотелось поделиться со всеми. Как минимум, так я сам не забуду. Ведь
Скажи мне — и я забуду, покажи мне — и я запомню, дай мне сделать — и я пойму.
— Конфуций и Бенджамин Франклин
В процессе изучения я пытался визуализировать взаимодействия сущностей: как объекты обращаются друг к другу, как один наследуется от другого и так далее. Найти иллюстрации не удалось, поэтому я нарисовал свои.
Я допускаю, что читатель знаком с JavaScript, знает о глобальном объекте, знает о том, что функции в JS являются функциями высшего порядка, и т.д.
Цепочка областей видимости
Когда JS-код работает, ему нужно пространство для хранения локальных переменных. Давайте назовем это пространство объектом области видимости (он же LexicalEnvironment — лексическое окружение) или просто scope-объектом. Например, когда вы вызываете какую-то функцию, а она задает локальную переменную, эта переменная сохраняется в объект области видимости. Можно считать его обычным JavaScript-объектом, с одним важным отличием: к нему нельзя обратиться напрямую. Можно изменять его свойства, но нельзя обратиться к самому объекту.
Концепция такого объекта для области видимости сильно отличается от, скажем, C или C++, где локальные переменные хранятся в стеке. В JavaScript такие объекты хранятся в heap, и они могут оставаться в памяти даже после того, как функция вернула значение. Об этом поговорим позже.
Как можно было ожидать, у объекта области видимости может быть родитель. Когда код пытается обратиться к переменной, интерпретатор ищет свойство у текущего объекта области видимости. Если свойства не существует, интерпретатор двигается вверх по цепочке scope-объектов и продолжает поиски. И так далее, пока свойство не найдено или пока не кончились родители. Давайте назовем этот порядок scope-объектов «цепочкой областей видимости» или «цепочкой scope-объектов».
Этот механизм очень похож на прототипное наследование, но, опять же, есть одно важное отличие: если вы попытаетесь обратиться к несуществующему свойству обычного объекта, и в цепочке прототипов нигде нет такого свойства, то это не ошибка — просто будет возвращен undefined. Но если обратиться к несуществующему свойству в scopе-цепочке (то есть обратиться к переменной, которой не существует), то будет ошибка ReferenceError.
Последний элемент в scope-цепочке это всегда глобальный объект (Global Object). В JavaScript-коде самого высокого уровня цепочка объектов областей видимости состоит из всего одного элемента: глобального объекта. Так что когда вы создаете переменные в верхнем уровне кода, они задаются в глобальном объекте. Когда происходит вызов функции, в scope-цепочке больше одного объекта. Можно подумать, что если функция вызвана из верхнего уровня, то в scope-цепочке есть ровно два объекта области видимости, но это не всегда так. Там может быть 2 или больше объектов, это зависит от функции. Об этом тоже позже.
Верхний уровень
Хватит теории, вот пример:
Мы просто создали две переменные на верхнем уровне. Как я объяснил выше, в этом случае объект области видимости это глобальный объект:
Тут у нас есть область запуска (это мой код верхнего уровня из my_script.js), и соответствующий scope-объект. Конечно, в реальности глобальный объект содержит еще кучу стандартных и специфических для хоста штук, но мы их тут не будем показывать.
Не-вложенные функции
Взгляните на такой скрипт:
Когда функция myFunc определена, идентификатор myFunc добавляется в текущий scope-объект (в данном случае — в глобальный объект), и этот идентификатор относится к функции. Как известно, функция это объект, поэтому далее когда мы говорим «функция-объект», то имеем ввиду объект, которым является функция.
Функция-объект содержит код функции и другие свойства. Одно из интересных нам свойств это внутреннее свойство [[scope]]; оно ссылается на текущий scope-объект, то есть scope-объект, который активен в момент определения функции (опять же — в данном случае это глобальный объект).
Когда мы вызываем console.log(“outside”), получается такая схема:
Функция-объект, на которую ссылается переменная myFunc, хранит код функции и ссылается на scope-объект, который был актуален в момент определения функции. Это очень важно.
Когда функция вызывается, создается новый scope-объект, который хранит локальные переменные для myFunc (и значения его аргументов), и этот новый scope-объект наследует от scope-объекта, на который ссылается вызываемая функция.
Так что, при вызове myFunc схема выглядит так:
Это — цепочка scope-объектов. Если обратиться к какой-нибудь переменной внутри myFunc, JavaScript попробует найти ее в первом объекте цепочки — области видимости функции myFunc(). Если там такой переменной нет, то нужно идти выше (в данном случае выше находится глобальный объект). Если найти ничего не получается во всей цепочке — будет ошибка ReferenceError.
Например, если обратиться к a внутри myFunc, то получим 1 из первого объекта — scope-объекта myFunc(). Если обратиться к foo, мы получим 3 из того же объекта: можно сказать, он скрывает свойство foo глобального объекта. Если обратиться к bar, то получим 2 из глобального объекта. Это работает почти как прототипное наследование.
Важно помнить, что эти scope-объекты продолжают существовать пока на них есть ссылки. Когда последняя ссылка на такой объект исчезает, объект будет обработан сборщиком мусора.
После того, как myFunc() возвращает значение, ссылок на область видимости myFunc() больше нет, сборщик мусора делает свое дело и получается:
Далее я не буду включать функции-объекты в диаграммы чтобы не перегружать иллюстрации. Как вы уже знаете, цепочка выглядит так: функция → функция-объект → scope-объект.
Не забывайте об этом.
Вложенные функции
С момента, когда функция возвращает значение, больше никто не обращается к его scope-объекту, поэтому его собирает сборщик мусора. Но что если определить вложенную функцию и вернуть ее (или сохранить где-то вне текущего scope-объекта) Вы уже знаете ответ: функция-объект всегда ссылается на scope-объект, в котором она была создана. Так что когда мы задаем вложенную функцию, она получает ссылку к текущей области видимости внешней функции. И если мы сохраним вложенную функцию в другом месте, то scope-объект не будет обработан сборщиком мусора даже когда внешняя функция вернет значение: ведь на этот scope-объект все еще есть ссылка! Взгляните на этот код:
При вызове createCounter(100); получается такая схема:
Заметьте, что на область видимости createCounter(100) существуют ссылки из вложенных функций increment и get. Если createCounter() не вернет ничего, то, конечно, эти внутренние ссылки на себя не будут считаться, и scope-объект будет собран сборщиком мусора. Но так как createCounter() возвращает объект, в котором есть ссылки на эти функции, получается так:
Итак, функция createCounter(100) уже вернула значение, но ее область видимости еще существует, она доступна из внутренних функций и только из них. Нет никакой возможности обратиться к области видимости createCounter(100) напрямую, можно только вызывать myCounter.increment() или myCounter.get(). У этих функций есть уникальный, частный доступ к области createCounter.
Давайте попробуем вызвать myCounter.get(). Помните — при вызове функции создается новая область видимости, и в цепочку областей видимости, которая используется для этой новой функции, добавляется новый объект. Получается так:
Первый scope-объект в цепочке функции get() это пустой scope-объект самой функции. Когда внутри get() происходит обращение к счетчику, JavaScript не может ничего найти в первом объекте цепочки, двигается к следующему объекту и использует счетчик в области видимости createCounter(100). И функция get() просто возвращает его.
Можно заметить, что объект myCounter также доступен функции myCounter.get() как ‘this’ (красная стрелка в диаграмме). this не является частью цепочки scope-объектов, но о нем нужно помнить. Об этом тоже позже.
Вызов increment(5) чуть более интересный, потому что здесь присутствует аргумент:
Важно понимать, что области видимости — «живые». При вызове функции текущая цепочка не копируется для функции, а на самом деле дополняется новым объектом. И когда любой объект цепочки изменяется, это изменение сразу становится доступным всем функциям, в цепочках которых этот объект состоит. После того, как increment() изменит значение счетчика, следующий вызов get() вернет обновленное значение.
Поэтому этот известный пример на работает:
Несколько функций создаются в цикле, и все они содержат ссылку на один и тот же scope-объект. Поэтому они используют одну и ту же переменную i, а не личную копию. Подробнее об этом примере можно почитать по ссылке Don’t make functions within a loop.
Похожие функции-объекты, разные scope-объекты
А теперь давайте немного расширим наш пример и развлечемся (да, мне весело — прим. пер.). Что если создать несколько объектов счетчиков?
После созданию myCounter1 и myCounter2 получается такая схема:
Не забывайте: каждая функция-объект содержит ссылку на scope-объект. В этом примере myCounter1.increment и myCounter2.increment ссылаются на функцию-объекты, которые содержат один и тот же код и одни и те же значения свойств (name, length и другие), но их [[scope]] ссылается на разные scope-объекты.
В диаграмме нет отдельных функция-объектов (для упрощения визуализации), но они все еще существуют.
Вот так это и работает. Концепция замыканий это мощь.
Цепочка scope-объектов и this
Нравится вам или нет, this не является частью цепочки scope-объектов. Значение this зависит от паттерна вызова функции. То есть можно вызывать одну и ту же функцию, но иметь разные значения this внутри.
Паттерны вызовов
На эту тему стоит написать отдельную статью, так что сейчас я просто пройдусь по теме поверхностно. Есть четыре паттерна. Вот:
Method invocation pattern (вызов метода)
Если вызов содержит точку или [subscript], то функция вызывается как метод. В примере выше this ссылается на myObj.
Function invocation pattern (вызов функции)
В этом случае значение this зависит от того, запущен ли код в strict mode.
В примере выше — strict mode, так что myFunc() вернет undefined.
Constructor invocation pattern (вызов конструктора)
Когда функция вызывается с префиксом new, JavaScript задает новый объект, который наследуется от свойства prototype указанной функции. И этот новосозданный объект передается в функцию как this.
Apply invocation pattern
Можно передать любое значение как this. В этом примере для этого используется Function.prototype.apply(). Другие варианты:
В следующих примерах в основном используется Method invocation pattern.
Использование this во вложенных функциях
Вывод: hidden: ‘value-in-closure’, myProp: ‘inner-value’
К моменту вызова myObj.createInnerObj() получается такая структура:
А к моменту вызова myInnerObj.innerFunc() — такая:
Видно, что this в myObj.createInnerObj() ссылается на myObj, но this в myInnerObj.innerFunc() ссылается на myInnerObj: обе функции вызваны как методы. Поэтому this.myProp внутри innerFunc() возвращает внутреннее значение, а не внешнее.
Можно обмануть innerFunc(), чтобы тот использовать myProp таким образом:
Вывод: hidden: ‘value-in-closure’, myProp: ‘fake-inner-value’
Или с apply() или call():
Вывод: hidden: ‘value-in-closure’, myProp: ‘fake-inner-value-2’
Однако, иногда внутренней функции на самом деле нужен доступ к this, который доступен во внешней функции, вне зависимости от того, как вызвана внутренняя функция. Для этого нужно специально сохранить нужное значение в замыкании (то есть в текущем scope-объекте) вот так: var self = this; и использовать self во внутренней функции вместо this.
Вывод: hidden: ‘value-in-closure’, myProp: ‘outer-value’
Теперь видно, что у innerFunc() есть доступ к значению this внешней функции, через self, который лежит в замыкании.
Заключение
Теперь мы можем ответить на те вопросы из первого абзаца.
Что такое замыкание? Это объект, связанный и с функцией-объектом и scope-объектом. На самом деле, все функции в JavaScript это замыкания: невозможно иметь ссылку на функцию-объект без scope-объекта.
Когда замыкание создается? Так как все функции в JavaScript это замыкания, ответ очевиден: когда задается функция — задается замыкание. Так что замыкание создается при определении функции. Но нужно понимать разницу между созданием замыкания и созданием нового scope-объекта: замыкание (функция + ссылка на текущую цепочку scope-объектов) создается при определении функции, но новый scope-объект создается (и используется для модификации цепочки scope-объектов замыкания) при вызове функции.
Когда замыкание уничтожается? Как любой другой объект в JavaScript, сборщик мусора обрабатывает замыкание когда на него больше нет ссылок.