Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

Замыкания, функции изнутри

Лекция



Привет, сегодня поговорим про замыкания функции изнутри, обещаю рассказать все что знаю. Для того чтобы лучше понимать что такое замыкания функции изнутри , настоятельно рекомендую прочитать все из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend).


  1. Лексическое окружение
    1. Пример
  2. Доступ ко внешним переменным
  3. Вложенные функции
  4. Управление памятью
  5. [[Scope]] для new Function
  6. Итого

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

Лексическое окружение

Все переменные внутри функции — это свойства специального внутреннего объектаLexicalEnvironment.

Мы будем называть этот объект «лексическое окружение» или просто «объект переменных».

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

В отличие от window, объект LexicalEnvironment является внутренним, он скрыт от прямого доступа.

Пример

Посмотрим пример, чтобы лучше понимать, как это работает:

1 function sayHi(name) {
2   var phrase = "Привет, " + name;
3   alert(phrase);
4 }
5  
6 sayHi('Вася');

 

При вызове функции:

  1. До выполнения первой строчки ее кода, на стадии инициализации, интерпретатор создает пустой объект LexicalEnvironment и заполняет его.

    В данном случае туда попадает аргумент name и единственная переменная phrase:

     

    1 function sayHi(name) {
    2   // LexicalEnvironment = { name: 'Вася', phrase: undefined }
    3   var phrase = "Привет, " + name;
    4   alert(phrase);
    5 }
    6  
    7 sayHi('Вася');

     

  2. Функция выполняется.

    Во время выполнения происходит присвоение локальной переменной phrase, то есть, другими словами, присвоение свойству LexicalEnvironment.phrase нового значения:

     

    1 function sayHi(name) {
    2   // LexicalEnvironment = { name: 'Вася', phrase: undefined }
    3   var phrase = "Привет, " + name;
    4  
    5   // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'}
    6   alert(phrase);
    7 }
    8  
    9 sayHi('Вася');

     

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

 

Тонкости спецификации

Если почитать спецификацию ECMA-262, то мы увидим, что речь идет о двух объектах: VariableEnvironment и LexicalEnvironment.

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

Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.

 

Доступ ко внешним переменным

Из функции мы можем обратиться не только к локальной переменной, но и к внешней:

 

1 var a = 5;
2  
3 function f() {
4   alert(a);  // 5
5 }

 

Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущемLexicalEnvironment, а затем, если ее нет — ищет во внешнем объекте переменных. В данном случае им является window.

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

Рассмотрим, как оно создается и используется в коде выше:

  1. Все начинается с момента создания функции. Функция создается не в вакууме, а в некотором лексическом окружении.

    В случае выше функция создается в глобальном лексическом окружении window:

    Замыкания, функции изнутри

    Для того, чтобы функция могла в будущем обратиться к внешним переменным, в момент создания она получает скрытое свойство [[Scope]], которое ссылается на лексическое окружение, в котором она была создана:

    Замыкания, функции изнутри

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

  2. Позже, приходит время и функция запускается.

    Интерпретатор вспоминает, что у нее есть свойство f.[[Scope]]:

    Замыкания, функции изнутри

    …И использует его при создании объекта переменных для функции.

    Новый объект LexicalEnvironment получает ссылку на «внешнее лексическое окружение» со значением из [[Scope]]. Эта ссылка используется для поиска переменных, которых нет в текущей функции.

    Замыкания, функции изнутри
    Например, alert(a) сначала ищет в текущем объекте переменных: он пустой. А потом, как показано зеленой стрелкой на рисунке ниже — по ссылке, во внешнем окружении.

    Замыкания, функции изнутри

    На уровне кода это выглядит как поиск во внешней области видимости, вне функции:

    Замыкания, функции изнутри

 

Если обобщить:

  • Каждая функция при создании получает ссылку [[Scope]] на объект с переменными, в контексте которого была создана.
  • При запуске функции создается новый объект с переменными. В него копируется ссылка на внешний объект из [[Scope]].
  • При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом — по этой ссылке. Благодаря этому в функции доступны внешние переменные.

 

 

Важность: 5

Каков будет результат выполнения этого кода?

 

01 var value = 0;
02  
03 function f() { 
04   if (1) {
05     value = true;
06   else {
07     var value = false;
08   }
09  
10   alert(value);
11 }
12  
13 f();

 

Изменится ли внешняя переменная value ?

P.S. Какими будут ответы, если из строки var value = false убрать var?

Решение

Результатом будет true, т.к. var обработается и переменная будет создана до выполнения кода.

Соответственно, присвоение value=true сработает на локальной переменной, иalert выведет true.

Внешняя переменная не изменится.

P.S. Если var нет, то в функции переменная не будет найдена. Интерпретатор обратится за ней в window и изменит ее там.

Так что без var результат будет также true, но внешняя переменная изменится.

[Открыть задачу в новом окне]

 

 

Важность: 5

Каков будет результат выполнения этого кода? Почему?

01 function test() {
02    
03   alert(window);
04  
05   var window = 5;
06  
07   alert(window);
08 }
09  
10 test();

 

Решение

Результатом будет undefined, затем 5.

 

   
01 function test() {
02    
03   alert(window);
04  
05   var window = 5;
06  
07   alert(window);
08 }
09  
10 test();

 

Директива var обработается до начала выполнения кода функции. Об этом говорит сайт https://intellect.icu . Будет создана локальная переменная, т.е. свойство LexicalEnvironment:

LexicalEnvironment = {
  window: undefined
}

 

Когда выполнение кода начнется и сработает alert, он выведет локальную переменную.

Затем сработает присваивание, и второй alert выведет уже 5.

[Открыть задачу в новом окне]

Важность: 4

Каков будет результат выполнения кода? Почему?

 

1 var a = 5
2  
3 (function() {
4   alert(a)
5 })()

 

P.S. Подумайте хорошо! Здесь все ошибаются!
P.P.S. Внимание, здесь подводный камень! Ок, вы предупреждены.

Решение

Результат - ошибка. Попробуйте:

 

   
1 var a = 5
2  
3 (function() {
4   alert(a)
5 })()

 

Дело в том, что после var a = 5 нет точки с запятой.

JavaScript воспринимает этот код как если бы перевода строки не было:

 

   
1 var a = 5(function() {
2   alert(a)
3 })()

 

То есть, он пытается вызвать функцию 5, что и приводит к ошибке.

Если точку с запятой поставить, все будет хорошо:

 

   
1 var a = 5;
2  
3 (function() {
4   alert(a)
5 })()

 

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

[Открыть задачу в новом окне]

 

Вложенные функции

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

Как правило, это делают для вспомогательных операций, например в коде ниже для генерации сообщения используются makeMessage и getHello:

 

   
01 function sayHi(person) {
02    
03   var message = makeMessage(person);
04   alert( message );
05  
06   // ----- вспомогательные функции -----
07  
08   function getHello(age) {
09     return age >= 18 ? 'Здравствуйте' 'Привет';
10   }
11  
12   function makeMessage(person) {
13     return getHello(person.age) + ', ' + person.name;
14   
15 }
16  
17 sayHi({
18   name: 'Петька',
19   age: 17
20 }); // привет, Петька

 

Вложенные функции могут быть объявлены и как Function Declaration и как Function Expression.

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

То есть, при запуске внешней функции sayHi, в ее LexicalEnvironment попадают локальные переменные и вложенные Function Declaration. При этом Function Declaration сразу готовы к выполнению.

В примере выше при запуске sayHi(person) будет создан такой LexicalEnvironment:

 

1 LexicalEnvironment = {
2   person: переданный аргумент,
3   message: undefined,
4   getHello: function...,
5   makeMessage: function...
6 }

 

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

Вложенная функция имеет доступ к внешним переменным через [[Scope]].

  1. При создании любая функция, в том числе и вложенная, получает свойство [[Scope]], указывающее на объект переменных, в котором она была создана.
  2. При запуске она будет искать переменные сначала у себя, потом во внешнем объекте переменных, затем в более внешнем, и так далее.

Поэтому в примере выше из объявления функции makeMessage(person) можно убрать аргументperson.

Было:

1 function sayHi(person) {
2   ...
3   function makeMessage(person) {
4     return getHello(person.age) + ', ' + person.name;
5   
6 }
Станет:
1 function sayHi(person) {
2   ...
3   function makeMessage() { // убрали аргумент
4     // переменная person будет взята из внешнего объекта переменных
5     return getHello(person.age) + ', ' + person.name;
6   
7 }

 

Вложенную функцию можно возвратить.

Например, пусть sayHi не выдает alert тут же, а возвращает функцию, которая это делает:

 

   
01 function sayHi(person) {
02    
03   return function() { // (*)
04     var message = makeMessage(person); // (**)
05     alert( message );
06   };
07  
08   // ----- вспомогательные функции -----
09  
10   function getHello(age) {
11     return age >= 18 ? 'Здравствуйте' 'Привет';
12   }
13  
14   function makeMessage() {
15     return getHello(person.age) + ', ' + person.name;
16   
17 }
18  
19 var sayHiPete = sayHi({ name: 'Петька', age: 17 });
20 var sayHiOther = sayHi({ name: 'Василий Иванович', age: 35 });
21  
22 sayHiPete(); // эта функция может быть вызвана позже

 

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

Возвращаемая функция (*) при запуске будет иметь полный доступ к аргументам внешней функции, а также к другим вложенным функциям makeMessage и getHello, так как при создании она получает ссылку [[Scope]], которая указывает на текущий LexicalEnvironment. Переменные, которых нет в ней, например, person, будут взяты из него.

В частности, функция makeMessage при вызове в строке (**) будет взята из внешнего объекта переменных.

 

Внешний LexicalEnvironment, в свою очередь, может ссылаться на еще более внешнийLexicalEnvironment, и так далее.

Замыканием функции называется сама эта функция, плюс вся цепочкаLexicalEnvironment, которая при этом образуется.

Иногда говорят «переменная берется из замыкания». Это означает — из внешнего объекта переменных.

Можно сказать и по-другому: «замыкание — это функция и все внешние переменные, которые ей доступны».

 

Управление памятью

JavaScript устроен так, что любой объект и, в частности, функция, существует до тех пор, пока на него есть ссылка, пока он как-то доступен для вызова, обращения. Более подробно об управлении памятью — далее, в статье Управление памятью в JS и DOM.

Отсюда следует важное следствие при работе с замыканиями.

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

Посмотрим на примеры.

  • Обычно объект переменных удаляется по завершении работы функции. Даже если в нем есть объявление внутренней функции:
    1 function f() {
    2   var value = 123;
    3   function g() { } // g видна только изнутри
    4 }
    5  
    6 f();
    В коде выше внутренняя функция объявлена, но она осталась внутри. После окончания работыf() она станет недоступной для вызовов, так что будет убрана из памяти вместе с остальными локальными переменными.
  • …А вот в этом случае лексическое окружение, включая переменную a, будет сохранено:
    1 function f() {
    2   var a = Math.random();
    3  
    4   function g() { }
    5  
    6   return g;
    7 }
    8  
    9 var g = f(); // функция g будет жить и сохранит ссылку на объект переменных
  • Если f() будет вызываться много раз, а полученные функции будут сохраняться, например, складываться в массив, то будут сохраняться и объекты LexicalEnvironment с соответствующими значениями a:
    1 function f() {
    2   var a = Math.random();
    3  
    4   return function() { };
    5 }
    6  
    7 // 3 функции,
    8 // каждая ссылается на соответствующий объект LexicalEnvironment = {a: ...}
    9 var arr = [f(), f(), f()];
    Обратим внимание, что переменная a не используется в возвращаемой функции. Это означает, что браузерный оптимизатор может «де-факто» удалять ее из памяти. Все равно ведь никто не заметит.
  • В этом коде замыкание сначала сохраняется в памяти, а после удаления ссылки на g умирает:
    01 function f() {
    02   var a = Math.random();
    03  
    04   function g() { }
    05  
    06   return g;
    07 }
    08  
    09 var g = f();  // функция g жива
    10 // а значит в памяти остается соответствующий объект переменных
    11  
    12 g = null;     // ..а вот теперь память будет очищена

[[Scope]] для new Function

Есть одно исключение из общего правила присвоения [[Scope]].

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

 

   
1 var sum = new Function('a,b'' return a+b; ');
2  
3 var result = sum(1,2);
4 alert(result); // 3

 

То есть, функция создается вызовом new Function(params, code):

params
Параметры функции через запятую в виде строки.
code
Код функции в виде строки.

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

При создании функции с использованием new Function, ее свойство [[Scope]] ссылается не на текущий LexicalEnvironment, а на window.

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

Сначала обычное поведение:

 

   
01 var a = 1;
02 function getFunc() {
03   var a = 2;
04   
05   var func = function() { alert(a); };
06  
07   return func;
08 }
09  
10 getFunc()(); // 2, из LexicalEnvironment функции getFunc

 

А теперь — для функции, созданной через new Function:

 

   
01 var a = 1;
02 function getFunc() {
03   var a = 2;
04   
05   var func = new Function('''alert(a)'); 
06  
07   return func;
08 }
09  
10 getFunc()(); // 1, из window

 

Итого

  1. Все переменные и параметры функций являются свойствами объекта переменныхLexicalEnvironment. Каждый запуск функции создает новый такой объект.
    На верхнем уровне роль LexicalEnvironment играет «глобальный объект», в браузере этоwindow.
  2. При создании функция получает системное свойство [[Scope]], которое ссылается наLexicalEnvironment, в котором она была создана (кроме new Function).
  3. При запуске функции ее LexicalEnvironment ссылается на внешний, сохраненный в [[Scope]]. Переменные сначала ищутся в своем объекте, потом — в объекте по ссылке и так далее, вплоть до window.

Разрабатывать без замыканий в JavaScript почти невозможно. Вы еще не раз встретитесь с ними в следующих главах учебника.

К сожалению, в одной статье не просто дать все знания про замыкания функции изнутри. Но я - старался. Если ты проявишь интерес к раскрытию подробностей,я обязательно напишу продолжение! Надеюсь, что теперь ты понял что такое замыкания функции изнутри и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)

создано: 2014-10-07
обновлено: 2024-11-14
341



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)

Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)