Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Функции "изнутри", замыкания

  1. Порядок создания глобальных переменных и функций
    1. Вопросы на понимание
    2. Особенности window в IE<9
  2. Локальные переменные
  3. Доступ ко внешним переменным
  4. Вложенные функции
    1. [[Scope]] для new Function
  5. Итого

Механизм работы функций и переменных в JavaScript очень отличается от большинства языков.

В этой главе мы разберем его подробно, чтобы легко было перейти к более продвинутым темам.

В JavaScript все переменные и фунции являются свойствами специального объекта, который называется LexicalEnvironment (лексическое окружение).

Лексическое окружение верхнего уровня называют global object (глобальный объект). В браузере этот объект доступен как window.

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

Например:

var a = 5;   // создает и присваивает свойство window.a
alert(window.a); // 5

Переменные нельзя удалять

Несмотря на то, что переменная — свойство объекта, оператор delete window.a не сработает. Поэтому для имитации удаления переменной им обычно присваивают null.

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

Порядок создания глобальных переменных и функций

Перед выполнением первой строчки кода происходит инициализация.

Скрипт сканируется на предмет объявления функций вида Function Declaration, а затем — на предмет объявления переменных var .... Каждое такое объявление добавляется в лексическое окружение. При этом функции создаются сразу рабочие, а переменные равны undefined.

Затем код начинает выполняться, и уже на фазе выполнения происходит присвоение (=) значений переменным.

// При входе в область видимости, до выполнения кода (скрипт еще не запустился!)
*!*
// window = { f: function, a: undefined, g: undefined }
*/!*

var a = 5;   // window.a=undefined создаётся при инициализации (до выполнения)

function f(arg) { /*...*/ }  // window.f = function, при инициализации

var g = function(arg) { /*...*/ }; // window.g = undefined, при инициализации

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

alert("a" in window); // *!*true*/!*,  т.к. есть свойство window.a 
alert(a); // равно *!*undefined*/!*,  т.к. значение будет присвоено ниже
alert(f); // *!*function*/!*,  готовая к выполнению функция
alert(g); // *!*undefined*/!*, т.к. это переменная (значение присвоится ниже)

var a = 5;  
function f() { /*...*/ } 
var g = function() { /*...*/ }

Итак, в результате, к началу выполнения кода:

  1. Функции, объявленные как Function Declaration, создаются полностью и готовы к использованию.
  2. Переменные объявлены, но равны undefined. Присваивания выполнятся позже, когда выполнение дойдет до них.

Присвоение переменной без объявления

Переменную можно присвоить и без объявления var:

a = 5;

Такое присвоение, как и var a = 5, создает свойство window.a = 5. Отличие — в том, что переменная будет создана не на этапе входа в область видимости, а в момент присвоения.

Сравните два кода ниже.

Первый выведет undefined, т.к. переменная была добавлена в window на фазе иницилизации:

*!*
alert(a); // undefined
*/!*

var a = 5;

Второй код выведет ошибку, т.к. переменной ещё не существует:

*!*
alert(a); // error, a is not defined
*/!*

a = 5;

Вообще, рекомендуется всегда объявлять переменные через var.

Конструкции for, if... не влияют на область видимости

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

В JavaScript нет разницы между объявлением вне блока:

*!*var*/!* i;
{
  i = 5;
}
… И внутри него:
i = 5;
{
  *!*var*/!* i;
}

Также нет разницы между объявлением в цикле и вне его:

for (*!*var*/!* i=0; i<5; i++) { }
..И объявлением вне цикла:
*!*var i;*/!*
for (i=0; i<5; i++) { }

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

Не важно, где и сколько раз объявлена переменная

Объявлений var может быть сколько угодно:

var i = 10;

for (var i=0; i<20; i++) {
  ...
}

var i = 5;

Все var будут обработаны один раз, на фазе инициализации — будет создана переменная. На фазе исполнения объявления var будут проигнорированы — они уже были обработаны. Зато будут выполнены присвоения.

Вопросы на понимание

Каков будет результат кода?

if ("a" in window) {
    var a = 1;
}
alert(a);

Решение
Решение

Ответ: 1.

Посмотрим, почему.

  1. На стадии подготовки, из var a создается window.a:
    // window = {a:undefined}
    
    if ("a" in window) {
        var a = 1;
    }
    alert(a);
    
  2. Условие "a" in window является true, так что выполняется присваивание:
    // window = {a:undefined}
    
    if (true) {
        var a = 1
    }
    alert(a)
    

    В результате a становится 1.

Каков будет результат (перед a нет var)?

if ("a" in window) {
    a = 1;
}
alert(a);

Решение
Решение

Ответ: ошибка.

Переменной a нет, так что условие "a" in window не выполнится. В результате на последней строчке - обращение к неопределенной переменной.

if ("a" in window) {
    a = 1;
} 
alert(a);  // <-- error!

Каков будет результат (перед a нет var, а ниже есть)?

if ("a" in window) {
    a = 1;
}
var a;

alert(a);

Решение
Решение

Ответ: 1.

Переменная a создается до начала выполнения кода, так что условие "a" in window выполнится и сработает a = 1.

if ("a" in window) {
    a = 1;
}
var a;

alert(a); // 1

Каков будет результат кода? Почему?

var a = 5; 
 
function a() { }

alert(a);

P.S. Это задание — учебное, на понимание. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково.

Решение
Решение

Ответ: 5.

var a = 5; 
 
function a() { }

alert(a);

Чтобы понять, почему — разберём внимательно как работает этот код.

  1. До начала выполнения создаётся переменная a и функция a. Стандарт написан так, что функция имеет приоритет. Но в данном случае это неважно, потому что…
  2. ..Когда код начинает выполняться — срабатывает присваивание a = 5, перезаписывая a.
  3. Объявление Function Declaration на стадии выполнения игнорируется.
  4. В результате alert(a) выводит 5.

Особенности window в IE<9

Каждая глобальная переменная является свойством window… В том числе, и сам `window:

alert(window); 
alert(window.window);

Но здесь есть очень интересная разница, которая существенна для браузеров IE<9. А именно, в них «обычный» window и его свойства window.window, window.window.window… — разные объекты!

Запустите в IE<9:

alert(window == window.window); // false
alert(window.window == window.window.window); // true

Это всё могло бы быть чистой теорией, но у неё есть важные следствия.

  1. Переопределение переменной, у которой такое же имя, как и id элемента, в IE<9 приведет к ошибке:
    <div id="a">...</div>
    <script> 
      a = 5;    // ошибка в IE<9! Правильно будет "var a = 5"
      alert(a); // никогда не сработает
    </script>
    

    По всей видимости, здесь идёт перезапись во «внешнем» объекте window, поэтому — ошибка. А если сделать через var, то переменная попадает в правильный «внутренний» window.window и всё будет хорошо. Это лишь догадки, у автора нет доступа к исходным кодам IE.

  2. Присутствует баг с рекурсией через функцию-свойство window. Следующий код «умрет» в IE<9:
    <script>
    // рекурсия через функцию, явно назначенную во "внешний" `window`.
    window.recurse = function(times) {
      if (times !== 0) recurse(times-1);
    }
    
    recurse(13);
    </script>
    

    Этот пример выдаст ошибку только в настоящем IE8! Не IE9 в режиме эмуляции. Вообще, режим эмуляции позволяет отлавливать где-то 95% несовместимостей и проблем, а для оставшихся 5% вам нужен будет настоящий IE8 в виртуальной машине.

Локальные переменные

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

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

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

function sayHi(name) {
  var phrase = "Привет, " + name;
  alert(phrase);
}

sayHi('Вася');

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

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

    function sayHi(name) {
    *!*
      // LexicalEnvironment = { name: 'Вася', phrase: undefined }
    */!*
      var phrase = "Привет, " + name;
      alert(phrase);
    }
    
    sayHi('Вася');
    

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

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

    function sayHi(name) {
      // LexicalEnvironment = { name: 'Вася', phrase: undefined }
      var phrase = "Привет, " + name;
    
    *!*
      // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'}
    */!*
      alert(phrase);
    }
    
    sayHi('Вася');
    

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

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

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

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

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

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

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

var a = 5;

function f() {
  alert(a);  // 5
}

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

Рассмотрим повнимательнее, как это работает.

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

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

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

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

    На момент создания существовало только свойство f.[[Scope]]:

    .. Теперь же функция запущена и создает свой собственный объект с переменными.

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

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

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

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

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

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

var value = 0;

function f() {  
  if (1) {
    value = true;
  } else {
    var value = false;
  }

  alert(value);
}

f();

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

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

Решение
Решение

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

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

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

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

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

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

function test() {
  
  alert(window);

  var window = 5;
}

test();

Решение
Решение

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

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

LexicalEnvironment = {
  window: undefined
}

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

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

var a = 5

(function() {
  alert(a)
})()

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

Решение
Решение

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

var a = 5

(function() {
  alert(a)
})()

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

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

var a = 5(function() {
  alert(a)
})()

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

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

var a = 5;

(function() {
  alert(a)
})()

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

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

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

Например, создадим новую функцию g внутри f и вернем ее:

var a = 1, b = 2;

function f() {
  var a = 2;

  return function g() {  // Named Function Expression
    alert(a + b);
  };
}

var g = f();
g(); // 4

При выполнении функции f создается новая функция g.

Она получает ссылку g.[[Scope]] на лексическое окружение LexicalEnvironment, соответствующее текущему запуску f:

Обратите внимание, функция g определена как Named Function Expression, поэтому её имя g является внутренним и отсутствует в объекте переменных f.

На этом этапе пока есть только два объекта с переменными: для текущего запуска f, со ссылкой на внешний объект window. Функция g пока не начала выполняться, для нее создается только [[Scope]].

При вызове внутренней функции g в ней создается свой, новый объект с переменными и, таким образом, образуется цепочка LexicalEnvironment g -> f -> window:

var a = 1, b = 2;

// window 
function f() {
  var a = 2;

  // LexicalEnvironment для f -> window 
  return function g() { 
    // LexicalEnvironment для g ->  LexicalEnvironment для f 

*!*
    alert(a+b); // ищем переменные: LexEnv для g -> LexEnv для f -> window (*)
*/!*

  };

}

var g = f();
g(); // 4

Как ищется a? (*)
Собственный LexicalEnvironment функции g будет пустой, т.к. никаких переменных и аргументов в g нет.

Переменная a будет искаться сначала в нем {}, а затем — пойдет по ссылке во внешний LexicalEnvironment для f, который содержит {a : 2}, откуда и будет взята.

Как ищется b? (*)
При поиске b интерпретатор поищет ее в {}, затем во внешнем объекте {a : 2}, затем в window, откуда и возьмет значение.

Пока жива хоть одна внутренняя функция g —- жив и объект переменных внешней функции f.
Получается, что функция f завершилась, а ее переменные остались. И доступны к использованию из внутренней функции. Это и называется замыканием.

[[Scope]] для new Function

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

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

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

var a = 1;
function getFunc() {
  var a = 2;
 
*!*
  var func = function() { alert(a); };
*/!*

  return func; 
}

getFunc()(); // *!*2*/!*, из LexicalEnvironemnt функции getFunc

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

var a = 1;
function getFunc() {
  var a = 2;
 
*!*
  var func = new Function('', 'alert(a)');  
*/!*

  return func;
}

getFunc()(); // *!*1*/!*, из window

Итого

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

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


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!