- Порядок создания глобальных переменных и функций
- Локальные переменные
- Доступ ко внешним переменным
- Вложенные функции
- Итого
Механизм работы функций и переменных в 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() { /*...*/ }
Итак, в результате, к началу выполнения кода:
- Функции, объявленные как
Function Declaration, создаются полностью и готовы к использованию. - Переменные объявлены, но равны
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.
Посмотрим, почему.
- На стадии подготовки, из
var aсоздаетсяwindow.a:
// window = {a:undefined} if ("a" in window) { var a = 1; } alert(a); - Условие
"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);
Чтобы понять, почему — разберём внимательно как работает этот код.
- До начала выполнения создаётся переменная
aи функцияa. Стандарт написан так, что функция имеет приоритет. Но в данном случае это неважно, потому что… - ..Когда код начинает выполняться — срабатывает присваивание
a = 5, перезаписываяa. - Объявление
Function Declarationна стадии выполнения игнорируется. - В результате
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
Это всё могло бы быть чистой теорией, но у неё есть важные следствия.
- Переопределение переменной, у которой такое же имя, как и
idэлемента, в IE<9 приведет к ошибке:
<div id="a">...</div> <script> a = 5; // ошибка в IE<9! Правильно будет "var a = 5" alert(a); // никогда не сработает </script>
По всей видимости, здесь идёт перезапись во «внешнем» объекте
window, поэтому — ошибка. А если сделать черезvar, то переменная попадает в правильный «внутренний»window.windowи всё будет хорошо. Это лишь догадки, у автора нет доступа к исходным кодам IE. - Присутствует баг с рекурсией через функцию-свойство
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('Вася');
- При вызове функции, до выполнения первой строчки её кода, интерпретатор создает пустой объект
LexicalEnvironmentи заполняет его.В данном случае туда попадает аргумент
nameи единственная переменнаяphrase:function sayHi(name) { *!* // LexicalEnvironment = { name: 'Вася', phrase: undefined } */!* var phrase = "Привет, " + name; alert(phrase); } sayHi('Вася'); - Функция выполняется.
Происходит присвоение локальной переменной
phrase, которое является по сути присвоением свойствуLexicalEnvironment.phraseнового значения:function sayHi(name) { // LexicalEnvironment = { name: 'Вася', phrase: undefined } var phrase = "Привет, " + name; *!* // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'} */!* alert(phrase); } sayHi('Вася'); - В конце выполнения, объект с переменными обычно выбрасывается и память очищается.
Если глубже посмотреть в современную спецификацию ECMA-262, то речь идет о двух объектах: VariableEnvironment и LexicalEnvironment.
Но там же замечено, что в реализациях эти два объекта могут быть объединены в один. Так что мы избегаем лишних деталей и используем везде термин LexicalEnvironment, это достаточно точно позволяет описать происходящее.
Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.
Доступ ко внешним переменным
Из функции мы можем обратиться не только к локальной переменной, но и к внешней:
var a = 5;
function f() {
alert(a); // 5
}
При этом интерпретатор сначала пытается найти ее в текущем объекте с переменными, а затем — ищет ее во внешнем LexicalEnvironment. В данном случае им является window, т.е. выводится window.a.
Рассмотрим повнимательнее, как это работает.
- Все начинается с момента создания функции. Функция создается не в вакууме, а в текущем
LexicalEnvironment.В случае выше функция создается в глобальном лексическом окружении
window:
Для того, чтобы функция могла в будущем обратиться к внешним переменным, в момент создания она получает скрытое свойство
[[Scope]], которое ссылается на объект с переменными, в котором она была создана:
- Позже, приходит время и функция запускается.
На момент создания существовало только свойство
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
Итого
- Все переменные и параметры функций являются свойствами объекта переменных
LexicalEnvironment. Каждый запуск функции создает новый такой объект.
На верхнем уровне рольLexicalEnvironmentиграет «глобальный объект», в браузере этоwindow. - При создании функция получает системное свойство
[[Scope]], которое ссылается наLexicalEnvironment, в котором она была создана (кромеnew Function). - При запуске функции её
LexicalEnvironmentссылается на внешний, сохраненный в[[Scope]]. Переменные сначала ищутся в своём объекте, потом — в объекте по ссылке и так далее, вплоть доwindow.
Разрабатывать без замыканий в JavaScript почти невозможно. Вы еще не раз встретитесь с ними в следующих главах учебника.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.