JavaScript — область видимости функции, ее время жизни и замыкание (Closure)

Когда вы впервые встречаетесь, с замыкание, то это кажется самой безумной идеей. Однако, как только вы поймете, что функции JavaScript являются объектами и что это подразумевает, это покажется естественным следствием.

Переменные, которые вы создаете вне функции, на самом деле являются свойствами глобального объекта, вы можете видеть, что глобальный объект играет роль объекта для кода, который живет вне функции.

То есть, переменные, находящиеся в области видимости функции, образуют контекст выполнения.

В случае JavaScript правила области видимости очень просты, но только если вы понимаете, что работаете с объектами Function, а не с функциями.

В JavaScript переменные имеют область видимости. Это означает, что при определении тела функции создаваемые переменные доступны только внутри этой функции.

То есть все переменные, объявленные внутри функции, по сути являются «локальными» переменными. Все локальные переменные существуют только во время выполнения функции — они не являются свойствами объекта. То есть локальные переменные создаются заново каждый раз, когда функция выполняется.

В JavaScript объекты могут «содержать» другие объекты в качестве свойств.

Точно так же объект Function может содержать другие объекты в качестве свойств, но в дополнение объект Function может создавать дополнительные объекты Function при определении тела его функции.

Например:

var myFunction= function(a,b){
  var ans=a+b;
  var myInnerF = function(){
    alert(ans);
  };
  return ans;
};

Обратите внимание, что у нас есть новая функция myInnerF, созданная при объявлении myFunction.

В этом случае мы говорим, что myInnerF «вложен» внутри или содержится в myFunction.

Это не значит, что myInnerF каким-то образом является свойством объекта myFunction Function. Это означает, что myInnerF создается при определении myFunction. Это объявление новой функции, которая вложена в myFunction. Нет никакого смысла в том, что созданный объект Function каким-либо образом содержится или вложен в объект Function, на который ссылается myFunction.

Новый объект Function точно такой же, как и любой другой объект Function, и на него ссылается локальная переменная myInnerF тела функции.

Также обратите внимание, что новый объект Function создается каждый раз, когда выполняется объявление myFunction. Это часто является причиной неэффективности, потому что легко забыть, что функции действительно являются объектами Function. Когда myFunction завершает работу, локальная переменная myInnerF уничтожается и, в конечном итоге, также становится объектом, на который она ссылается, когда она собирается мусором в движке JavaScript.

На данный момент кажется, что объявление функции внутри другой функции просто меняет продолжительность жизни внутренней функции, но это еще не все.

«Вложенная» область означает, что myInnerF может не только обращаться к своим собственным локальным переменным, но и к любым переменным, которые являются локальными для содержащей функции, т.е. в этом случае myFunction. Как вы можете видеть в этом примере, он использует ans, который является локальным для myFunction.

Вложенная идея применяется, когда несколько функций вложены друг в друга. Каждая внутренняя функция имеет доступ к своим собственным локальным переменным и переменным всех функций, которые содержат ее.

Таким образом, в этом случае и myFunction, и myInnerF также имеют доступ к переменным, определенным в глобальном объекте. В этом смысле все функции вложены в глобальный объект.

Это понятие вложенной области видимости, когда функции являются объектами, является очень тонкой идеей.

Причина в том, что когда вы смотрите на тело функции объекта Function, возникает тенденция думать, что он выполняется во время чтения. Это не тот случай. Если вы посмотрите на наш пример, сначала создается внешний объект Function и сохраняется его тело функции. Когда вы выполняете внешнее тело функции, через некоторое время создается внутренний объект Function, но его код не выполняется, пока он не будет вызван, возможно, в гораздо более позднее время. В этом конкретном примере внутренняя функция не оценивается, но может быть, и это имеет некоторые странные последствия из-за вложенной области видимости.

Время жизни
Тот факт, что функция на самом деле является объектом функции, позволяет ей существовать самостоятельно независимо от того, как и когда оценивается ее тело функции. Локальные переменные функции существуют только тогда, когда она выполняется, но ее свойства существуют всегда. Это еще одна странная идея, если вы знакомы с функциями на других языках.

Если вы собираетесь понять некоторые из более тонких способов работы JavaScript, вы должны быть на 100% иметь понимание времени жизни объекта функции.

Когда объект Function создается как часть некоторого кода, объект Function создается и существует во всей программе, как и любой другой объект.

Как уже объяснялось, JavaScript обеспечивает автоматическую сборку мусора, которая удаляет любой объект, на который нет ссылок. Таким образом, объект, любой объект, включая объект Function, существует до тех пор, пока существует хотя бы одна ссылка на него.

Теперь рассмотрим значение для вложенной функции, которое мы рассмотрели в предыдущем разделе:

var myFunction= function(a,b){
  var ans=a+b;
  var myInnerF=function(){
    alert(ans); 
  }; 
  return ans; 
};

Как уже объяснялось, внутренний объект Function, на который ссылается myInnerF, не существует до тех пор, пока внешняя функция не будет вызвана. Вот когда ты пишешь:

myFunction(1,2);

внутренний объект Function создается, когда система достигает линии

var myInner= etc.

Этот объект Function существует с этой точки до конца внешней функции, то есть до тех пор, пока не будет возвращен ans;

В этот момент переменная myInner выходит из области видимости и больше не существует. Поскольку переменные, ссылающиеся на внутренний объект Function, отсутствуют, то они быстро собираются и, таким образом, вступают в силу, как только внешняя функция завершает свою работу, внутренняя функция больше не существует — не совсем верно, но достаточно близко для большинства целей.

Так что это немного ограничивает в том смысле, что единственный код, который может когда-либо оценивать внутреннюю функцию, должен быть частью тела внешней функции. Причина в том, что это единственное место, где переменная myInnerF находится в области видимости.

Теперь рассмотрим, что происходит, если внутренняя функция имеет переменную, которая ссылается на нее, которая не является локальной для внешней функции.

В этом случае внутренняя функция может фактически жить дольше, чем внешняя функция, потому что у нее все еще есть переменная, которая ссылается на нее, и, следовательно, она не будет собирать мусор.

То есть, не только внутренний объект функции может существовать после завершения внешней функции, он может существовать и после уничтожения объекта внешней функции. Их время жизни совершенно не зависит друг от друга.

Это еще одна странная идея, но совершенно естественная, если вы думаете с точки зрения объектов Function.

Функциональные объекты остаются живыми и потенциально активными, пока существует переменная, которая ссылается на них.

Звучит сложно — но ты к этому привыкаешь.

Например:

var myFunction= function(a,b){
 var ans=a+b;
 this.myInnerF=function(){
   alert(ans);
  };
 return ans;
};

Теперь на внутреннюю функцию ссылается глобальная переменная — напомним, что она ссылается на глобальный объект — чтобы быть более точным, она ссылается на контекст выполнения, но об этом позже.

Примечание: если вы хотите использовать это в строгом режиме, убедитесь, что вы объявили myInnerF как глобальную переменную вне функции.

Теперь мы можем вызвать внешнюю функцию:

myFunction(1,2);

и когда функция возвращает глобальную переменную, myInner ссылается на объект Function, созданный во внешней функции.

Поскольку до сих пор есть ссылка на внутренний объект Function, он не является сборщиком мусора и, таким образом, все еще существует после завершения внешней функции.

Это означает, что вы все еще можете оценивать внутреннюю функцию спустя долгое время после того, как тело функции, которая ее создала, завершило выполнение и, возможно, даже спустя долгое время после того, как внешний объект Function, который объявляет свое тело функции, был уничтожен.

Например:

this.myInnerF();

отобразит окно Alert, и вы увидите значение в ans во время создания внутренней функции, даже если внешняя функция закончила работать некоторое время назад и все ее локальные переменные больше не существуют — напомним, что локальные переменные существуют только тогда, когда функция вызывается.

Вы можете видеть, что, позволяя функциям иметь вложенную область видимости, мы должны позволить внутренней функции использовать локальные переменные, принадлежащие внешней функции, даже после того, как эти локальные переменные были уничтожены, потому что внешняя функция завершена.

Хорошо, это все хорошо, но как внутренняя функция может иметь доступ к переменной — ans — которая больше не существует?

Это и есть замыкания.

Замыкания (Closures)
Поскольку время жизни объекта Function зависит только от того, какие переменные ссылаются на него, как и на любой другой объект, он может существовать намного позже любой внешней функции, которая его создала.

По правилам вложенной области видимости внутренняя функция имеет доступ к переменным во внешней функции при ее выполнении.

Единственная проблема с этим состоит в том, что локальные переменные внешней функции существуют только во время ее выполнения, и это означает, что в принципе внутренняя функция может получить к ним доступ, только если она выполняется как часть кода внешней функции, т.е. только в течение времени, когда внешняя функция выполняется.

Идея замыкания просто расширяет этот доступ на все время.

Другими словами, вложенная область действия сохраняется за пределами определения внешней функции и даже за пределами времени жизни внешней функции и всех ее локальных переменных.

Если бы это было не так, то иметь внутренние функции с их собственным временем жизни и вложенной областью было бы очень сложно.

Правило должно быть примерно таким: внутренняя функция имеет доступ к локальным переменным внешней функции, но только во время вызова функции. Если вы вызвали внутреннюю функцию позднее, вы бы просто получили сообщение об ошибке, потому что переменные, к которым вы могли получить доступ за один раз, больше не доступны.

Это будет означать, что вы получите другой результат в зависимости от того, когда и где вы вызвали внутреннюю функцию.

Использование замыкания означает, что внутренняя функция всегда имеет доступ к одним и тем же переменным, независимо от того, где и когда она вызывается.

Точнее, когда создается объект Function, все переменные, которые находятся в области видимости, включая переменные любых внешних функций, сохраняются в контексте выполнения — это было объяснено в самом начале.

Новым является то, что контекст выполнения остается с объектом Function, даже когда переменные больше не существуют, потому что тело создавшей их функции завершило работу или объект Function, к которому он принадлежит, был собран сборщиком мусора.

Иными словами, контекст выполнения функции остается контекстом выполнения до тех пор, пока он существует.

Есть важное исключение из этого правила.

Функции, созданные с помощью конструктора Function, всегда создаются в глобальной области видимости и не создают замыканий с использованием текущего контекста выполнения.

В качестве другого примера рассмотрим:

var myFunction= function(){
  var message="Hello Closure";
   this.myInnerF=function(){
                   alert(message);
                   message="Goodbye Closure";
                 };
};

Первая строка myFunction создает сообщение локальной переменной, которое добавляется в контекст выполнения и устанавливается на «Hello Closure». Затем определяется внутренняя функция, и ссылка на нее сохраняется в глобальной переменной myInnerF. Тело функции показывает окно предупреждения с тем, что хранится в сообщении, а затем оно изменяет сообщение. Это демонстрирует, что вы можете получить доступ и изменить то, что хранится в переменной, которая предоставляется из замыкания.

С этим определением вы можете теперь сделать следующее. Сначала вы можете вызвать myFunction;

myFunction();

Это определяет тело функции и, следовательно, приводит к созданию объекта Function с телом функции:

this.myInnerF=function(){
                 alert(message); 
                 message="Goodbye Closure";
               };

Еще раз обратите внимание, что внутренняя функция не вызывается, она просто устанавливает объект Function и его тело функции.

Теперь вы можете установить переменную myFunction в значение null, что приведет к тому, что больше не будет ссылок на внешний объект Function, и поэтому он будет собирать мусор — то есть его объект Function больше не существует и переменная сообщения больше не должна существовать.

Тем не менее, вы все равно можете вызвать внутренний объект Function. Первый раз:

this.myInnerF();

ты увидишь:

"Hello Closure"

и во второй раз вы оцените это:

"Goodbye Closure"

Кажется, переменная сообщения жива и здорова, даже если объект Function и его тело функции, к которому он относится, скорее всего, нет.

Когда вы впервые сталкиваетесь с идеей замыкания, это кажется сложным и произвольным — зачем так поступать?

Зачем придумывать идею замыкания?

Как только вы поймете, что функции — это просто объекты, и объект Function может жить дольше, чем объект Function, который его создал, и появляется вложенная область действия, и вы начинаете понимать, почему замыкания являются естественными.

Они являются естественным следствием предоставления внутренней функции доступа к переменным всех внешних функций, которые ее содержат.

Чтобы это работало, объект Function должен охватывать все переменные, которые находятся в области видимости при его создании, и их значения после завершения внешних функций.

Этот последний пункт является трудно уловимый.

Посмотрите, сможете ли вы выяснить, что произойдет, если мы внесем небольшое изменение в последний пример, и если вы сможете также выяснить, почему это происходит:

var myFunction= function(){
   this.myInnerF=function(){
                   alert(message);
                   message="Goodbye Closure";
                 };
   var message="Hello Closure";
};

Единственное изменение заключается в том, что теперь message определяется после внутренней функции. Вы можете предположить, что message не находится в области видимости.

Если вы помните, все объявления переменных перемещаются компилятором в начало функции, в которой они встречаются.

Таким образом, код эквивалентен:

var myFunction= function(){
  var message;
  this.myInnerF=function(){ 
                  alert(message); 
                  message="Goodbye Closure";
                };
  message="Hello Closure"; 
};

Это означает, что переменная находится в области видимости, то есть после определения внутренней функции, ее окончательное значение является частью замыкания. Причина в том, что контекст выполнения хранит переменные и их изменяющиеся значения до тех пор, пока внешняя функция не остановится.

То есть контекст выполнения фиксирует окончательное значение локальных переменных внешних функций. Таким образом, новая версия по-прежнему отображает два сообщения, как и раньше.

Переведено с ресурса: i-programmer.info/programming/javascript/11759-just-javascript-functions-scope-lifetime-and-closure.html

Добавить комментарий