Pull to refresh

Лексическая область видимости функций в JavaScript

Reading time5 min
Views39K
Почитав недавние посты для новичков JavaScript, решил написать небольшой топик про один интересный вопрос, которого ни один из авторов пока не касался, а именно, вопрос про область видимости функций в JavaScript.

Как гласит википедия: Функциям в ECMAScript присуща лексическая область видимости. Это означает, что область видимости определяется в момент определения функции (в отличие от динамической области видимости, при которой область видимости определяется в момент вызова функции).
Собственно написано все довольно коротко и ясно, но давайте разберем на практике такой пример:

var y = 5;
var x = function(){
    return y;
};
var z = function(t){
    var y = 10;
    return t();
};
z(x);

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

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



На картинке видно, как будет происходить поиск переменных. Выглядит это примерно так: z — локальная переменная функции, она будет найдена непосредственно в объекте вызова, x в объекте вызова отсутствует, поэтому поиск будет производиться выше по цепочке. Поиск переменных производится вплоть до глобального объекта, если и в нем переменная не будет найдена, то результатом будет значение undefined.

Итак, теперь мы готовы разобрать первый пример подробно.

// объявляем глобальную переменную "y" = 5
var y = 5; 
// объявляем глобальную переменную содержащую функцию, 
// учитываем что для нее будет "сохранен" контекст, 
// а именно объект вызова и глобальный объект.
var x = function(){
    // возвращаем значение переменной "y". 
    // т.к. локальная переменная "y" отсутствует, 
    // поиск будет произведен в вышестоящем объекте 
    // цепочки областей видимости (в глобальном объекте)
    return y;
};
// объявляем еще одну глобальную переменную содержащую функцию,
// принимающую в качестве аргумента другую функцию
var z = function(t){
    // внутри функции объявляем локальную переменную "y"=10,
    // она перекроет глобальную переменную "y"=5
    var y = 10;
    // возвращаем результат выполнения функции,
    // переданной в качестве аргумента.
    // ВСПОМИНАЕМ, что вместе с функцией хранится и передается 
    // контекст в котором функция была объявлена!
    return t();
};
// вызываем объявленную функцию, 
// передав в качестве аргумента функцию "x" объявленную ранее.
// Не забываем про контекст функции "x"!
z(x);


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

Здесь есть одна тонкость. Контекст (цепочка областей видимости) в которой была объявлена функция, представляет из себя грубо говоря цепочку объектов. Это очень важно понять, так как объекты в JavaScript хранятся в виде ссылок, то есть каждый элемент цепочки ссылается на определенный объект (именно ссылается а не содержит его копию), который в ходе выполнения программы может меняться. То есть объект из цепочки области видимости может меняться какими то внешними функциями, при этом все изменения будут доступны и в функции в чью цепочку областей видимости входит изменяемых объект. Другими словами цепочка областей видимости фиксируется в момент определения функции, но перечень свойств, определенных в этой цепочке, не фиксируется. Цепочка областей видимости подвержена изменениям, и функция может обращаться ко всем элементам, существующим на момент исполнения. Это очень важный абзац, если вы не уверены что поняли его точно, прочтите его еще раз. После того, как вы осознаете все написанное в нем, вам откроется дверь к созданию мощных эффектов, которые обычно и называют замыканиями.

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

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

Вместо выводов: Надеюсь описанный выше пример наглядно демонстрирует что такое лексическая область видимости и чем она отличается от динамической.

Для того что бы убедиться, что вы осознали всю прелесть замыканий и поняли что такое лексическая область видимости функций, рассмотрите еще один небольшой пример:
var y = 5;
var x = function(){
    return y;
};
var z = function(t){
    var y = 10;
    return t();
};
y = 15;
z(x);


P.S. В комментариях, уважаемый хабра-пользователь jeje приводит интересный пример, который тоже стоит рассмотреть:

var y = 5;
var x = function(){
    return y;
};
var z = function(t){
    y = 10; //это не объявление переменной, это ссылка на глобальную переменную
    return t();
};
z(x);


Этот пример отличается от первоначального тем, что внутри функции z() переменная y используется без ключевого слова var. В результате выполнения этого примера мы получим на выходе число 10. Почему это происходит? На самом деле мы уже ответили на этот вопрос, это происходит потому что внутри функции z() мы уже не объявляем локальную переменную y, а ссылаемся на глобальную.

Этот пример затрагивает очень важный вопрос — вопрос неявного объявления переменных в JavaScript. Когда внутри функции мы используем переменную без ключевого слова var интерпретатор выполняет следующие операции: Производится поиск переменной в объекте вызова (поиск локальных переменных), если переменная не найдена, то поиск производится в объекте находящемся выше в цепочке областей видимости и так вплоть до глобального объекта. Если переменная будет найдена в каком либо объекте, то ее значение будет изменено, так осуществляется доступ к глобальным переменным. Есть только одно НО: если переменная не будет найдена ни в одном объекте в цепочке областей видимости, то интерпретатор JavaScript объявит используемую переменную автоматически и присвоит значение ей, при этом переменная будет создана в глобальной области видимости. Учитывая это, нужно уделить особое внимание переменным функции, используемым без ключевого слова var. У явного и неявного объявления переменных есть и другие интересные особенности, но это тема другого микро-топика.
Tags:
Hubs:
Total votes 65: ↑54 and ↓11+43
Comments49

Articles