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

    Почитав недавние посты для новичков 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. У явного и неявного объявления переменных есть и другие интересные особенности, но это тема другого микро-топика.
    Поделиться публикацией

    Комментарии 49

      +5
      помимо контекста важна еще и такая деталь, как
      var y = 10;
      
      потому что заменив её на
      y = 10;
      
      мы получим желаемый результат
        0
        Я бы даже добавил это в статью как пример модификации переменной глобального контекста. Важный момент, imho.
          0
          Результат то, кстати, может как раз и не желаемый в большинстве случаев, но такое поведение надо понимать.
            +1
            На самом деле, в топике объясняется почему это происходит, но пример хороший, упомянуть действительно стоит. Сейчас оформим.
            +2
            Куцо как-то.
            Например, «всплытие» не разобрано.
              +1
              Была неплохая статья на эту тему: habrahabr.ru/post/127482/
                0
                Я пока не эксперт, поясните подробнее. Под всплытием имеется ввиду объявление переменных в конце тела функции? Если так, то действительно стоит рассмотреть этот вопрос, но это уже особенности явного и неявного объявления переменных — это тема другого микро-топика.
                  0
                  > Под всплытием имеется ввиду объявление переменных в конце тела функции?
                  Можно сказать и так.
                  Это тема напрямую относится к понятию scope.
                    0
                    Я думаю этот вопрос все же лучше рассмотреть отдельно, потому что если рассматривать их в одном топике, топик будет слишком сложным.
                  +1
                  Еще такой пример, чтобы показать, что в замыкание попадает именно переменная, а не ее значение:

                  function counter(i) {
                      inc = function() { alert(++i) };
                      dec = function() { alert(--i) };
                  }
                  
                  counter(0);
                  inc(), inc(), dec();
                  
                    –1
                    Толково написано.
                    Вот только когда речь идёт о замыканиях, сразу вспоминаются утечки памяти — и на этом аспекте хорошо бы остановиться подробнее. Дабы начиная использовать этот мощный инструмент JS, люди не делали таких трудноотлавливаемых ошибок.
                      0
                      Утечки памяти это в первую очередь особенность реализации интерпретатора. То есть попросту говоря они зависят от того где используется ваш код. Я пока не готов подробно описать утечки, просто времени не хватает. :)
                      +2
                      Ещё стоит упомянуть о не совсем очевидном моменте jsbin.com/ozeyoz/1/
                      var my_var = 'глобальная';
                      
                      function my_function() {
                        var my_var = 'локальная';
                        var some_function = new Function('console.log(my_var)');
                        some_function();
                      }
                      
                      my_function(); // 'глобальная'
                        0
                        Да, я знаю, это редко используемый кейс.
                          0
                          Тело функции переданное строкой в конструктор Function привязывается к глобальному контексту?
                            0
                            Ну да, можно и так сказать.

                            Насколько я понимаю, через конструктор Function() функция создается в глобальном контексте, и локальный, соответственно не использует.
                          +5
                          Неделя основ Javascript'a на хабре!

                          Наверняка будет полезно.

                          По моим наблюдениям, большинство веб-разработчиков начинают изучение javascript'a неосновательно. Так, будто это не полноценный язык, а надстройка над html для создания красивостей.
                            0
                            При этом большинство думает, что это Паскаль с другим синтаксисом.
                            +2
                            Вы извините, но судя по тому что за основной источник информации вы взяли википедию, а не стандарт, читать статью даже не хочется.

                            Почему вы ни слова не сказали о том, что ECMAScript 5 определяет такие понятие как лексическое окружение и записи лексического окружения?

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

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

                            Этот пример затрагивает очень важный вопрос — вопрос неявного объявления переменных в JavaScript.


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

                            Есть только одно НО: если переменная не будет найдена ни в одном объекте в цепочке областей видимости, то интерпретатор JavaScript объявит используемую переменную автоматически и присвоит значение ей

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

                            После того, как вы осознаете все написанное в нем, вам откроется дверь к созданию мощных эффектов, которые обычно и называют замыканиями

                            Замыкание — это совокупность тела любой функции и ее лексического окружения
                            Я что-ли должен продолжать или вы все-таки сами?

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


                            Издеваетесь?
                            Почему ни слова о функции eval, конструкторе Function, инструкции with?
                            Покажите место в стандарте где хоть слово есть о динамической области видимости.

                            Если у подняли такую сложную тему, то добавьте соответствующий материал:

                            Lexical scope, Lexical environments, Environment Record, Reference Specification Type, Closure, Hoisting

                            Не подумайте что я придираюсь, но читать стандарт перед написанием таких тем и знать о том что тема уже раскрыта (1, 2, 3, 4, 5, 6, 7) в других местах просто необходимо.

                            Пара примеров на вскидку:

                            alert(foo) // ?
                            
                            foo = function() {
                            	return 1;
                            };
                            


                            var foo = function() {
                            	var i = 0;
                            
                            	foo = function() {
                            		return i++;
                            	}
                            
                            	return foo();
                            };
                            
                            foo(); // ?
                            foo(); // ?
                            foo(); // ?
                            


                            alert(foo) // ?
                            
                            foo = function() {
                            	return 1;
                            };
                            


                            Если интересно, то у меня есть небольшая библиотека на тему замыканий и частичного применения функций

                              0
                              И еще один пример вдогонку:

                              void function() {
                              	'use strict';
                              	
                              	alert(this);                     // ?
                              	alert(eval('this'));             // ?
                                      alert((null, eval)('this'));      // ?
                              	alert(Function('return this')()) // ?
                              }();
                              
                                0
                                Извините, я исправлюсь :)

                                Но по поводу ваших замечаний, я соглашусь не со всеми:

                                «Стандарт определяет только один тип переменных.,
                                Отсутствие спецификатора var при объявлении — свидетельствует об инициализации свойства глобального объекта.»
                                Во-первых, я не говорил про тип переменных, вы что то спутали, я говорил про способы объявления переменной. А как вы отличаете «Отсутствие спецификатора var при объявлении» от обращения к переменной объявленной в объемлющей функции?

                                «Почему ни слова о функции eval, конструкторе Function, инструкции with?» Потому что не было у меня такой цели. Почитайте пожалуйста заголовок топика. Более того, я бы с удовольствием прочитал вашу заметку об этом, написать свою пока не могу, не хватает времени.

                                «Не подумайте что я придираюсь, но читать стандарт перед написанием таких тем и знать о том что тема уже раскрыта (1, 2, 3, 4, 5, 6, 7) в других местах просто необходимо.»

                                Автор на которого вы ссылаетесь, мне незнаком. Изучу на досуге.
                                0
                                А как вы отличаете «Отсутствие спецификатора var при объявлении» от обращения к переменной объявленной в объемлющей функции?:

                                foo = 1;
                                var bar = 1;
                                
                                void function () {
                                    alert(['foo' in this, 'bar' in this]); // [true, false];
                                }();​
                                
                                  0
                                  Промахнулся с ответом*
                                    0
                                    Спасибо, пример хороший, но то что foo = 1; это объявление переменной — это не очевидно.
                                      0
                                      Поправка, конечно же речь об инициализации свойства глобального объекта. :)
                                      0
                                      Не батенька, в обоих случаях true.
                                        0
                                        Вообще это не я писал, но в целом поддерживаю, у меня тоже true в обоих случаях
                                          0
                                          Ну вообще, ответ ему я и прислал, а вам пришло уведомление потому что вы автор топика.
                                          0
                                          Пример нужно запускать в браузере, а не в консоле.
                                            0
                                            Дык понятно, что не в консоле. А вы попробуйте в примере убрать галочку — onload — jsfiddle.net/tEvnj/1/
                                              0
                                              Вы действительно не понимаете разницы между свойство и переменной?
                                              foo = 1;
                                              var bar = 1;
                                              
                                              delete foo; // true
                                              delete bar; // false
                                              
                                              alert(bar); // 1
                                              alert(foo); // RefferenceError 
                                              
                                                0
                                                Пол часа искал:
                                                «Если присвоить значение переменной, не объявленной с помощью инструкции var, JavaScript неявно объявит эту переменную за вас. Однако переменные, объявленные таким образом, всегда создаются как глобальные, даже если они работают только в теле функции. Чтобы не создавать глобальную переменную (или не использовать существующую), когда достаточно локальной переменной для отдельной функции, всегда помещайте инструкцию var в тело функции. Лучше всего объявлять с ключевым словом var все переменные – и глобальные, и локальные.»

                                                Дэвид Флэнаган. JavaScript. Подробное руководство. 5-е издание. стр. 69
                                                  +2
                                                  Со всем уважением к Дэвиду Флэнагану, но в этой ситуации он лукавит, т.к. никакого неявного объявления переменной не происходит, и это легко проверить, т.к. в отличии от свойств (в данном случае речь идет о свойствах глобального объекта) переменные нельзя удалить.

                                                  В этом правиле есть только одно исключение и оно четко прописано в стандарте:

                                                  'use strict';
                                                  global = 1;
                                                  alert(global) // ReferenceError
                                                  


                                                  Также советую посмотреть раздел 8.7.2 PutValue (V, W)

                                                  Насчет JavaScript — это реализация стандарта ECMAScript, при этом каждая реализация оставляет за собой право расширять предоставляемый функционал. Самый очевидный пример DOM API.
                                                    0
                                                    Ну а все же, ничего не посоветуете из литературы кроме стандарта?
                                                    0
                                                    Собственно мой коммент ниже для вас тоже подайдёт.
                                                      0
                                                      Сегодня снова нашел упоминание про объявление переменных без ключевого слова var, но уже у другого автора:

                                                      Nicholas C. Zakas — Professional JavaScript for Web Developers (Programmer to Programmer) — 2011:

                                                      Although it’s possible to define global variables by omitting the var operator, this approach is not recommended. Global variables defined locally are hard to maintain and cause confusion, because it’s not immediately apparent if the omission of var was intentional. Strict mode throws a ReferenceError when an undeclared variable is assigned a value.

                                                      Все таки интересно, это намеренное упрощение, или же нормальных книг по JS нет?
                                                        0
                                                        Я думаю намеренное. Видимо, большое количество ошибок, которое авторы в своей практике выловили, было связано с пропуском «var». Поэтому от греха подальше решили предупреждать об этом всегда.
                                                          0
                                                          Дело не в том, что они предупреждают об этом. Дело в том что, ввиду дискуссии выше, кажется это предупреждение не совсем точно.
                                                    0
                                                    Цитата приведена не для того что бы холивар разводить. А только для подтверждения моих слов. Если эта информация неточна, дайте ссылочку на источник, где можно пробелы в знаниях заполнить. :)
                                                      0
                                                      Ну конечно нет, не знаю. Но это не важно. Я всего лишь указал на то, что ваш пример неправилен, вне контекста правы ли вы в этой ситуации или нет. У вас просто там было ошибка.

                                                      Далее, по поводу спора.

                                                          var test = 123;
                                                          
                                                          var pd = Object.getOwnPropertyDescriptor(this, 'test');
                                                          
                                                          pd.value = 321;
                                                          
                                                          Object.defineProperty(this, 'test2', pd);
                                                          
                                                          delete test;
                                                          delete test2;
                                                         
                                                          console.log(test);
                                                          console.log(test2);
                                                      


                                                      Вообще, по хорошему, вы правы. Переменная это не тоже самое, что свойство. Но в частном случае глобального контектса — это тоже самое. Ну и ещё, если добавить в pd, writable = false, то получится эмуляция const.
                                                        0
                                                        Но в частном случае глобального контектса — это тоже самое


                                                        Неужели?

                                                        alert(bar) // undefined
                                                        alert(foo) // Refference Error
                                                        
                                                        var bar = 1;
                                                        foo = 1;
                                                        


                                                        Еще более интересный пример:

                                                        alert(bar) // undefined
                                                        alert(foo) // Refference Error
                                                        
                                                        if (true) {
                                                            var bar = 1;
                                                            foo = 1;
                                                        }
                                                        


                                                        В первом своем комментарии к этой статье, я уже приводил аналогичный пример.
                                                          0
                                                          Аглоритм объявления переменных и так понятен, непонятно, что в нём вы такого интересного нашли, разве что для тех, кто только его узнал.

                                                          Но давайте разберёмся. Для глобального контекста:

                                                          1. Объявление переменных и своств не одно и тоже (Разные алгоритмы соответственно).
                                                          2. Обращение к переменным и свосвам может быть взаимо заменяемое. (С учётом правил конечно, первого пункта).

                                                          var foo;
                                                          this.bar = void 0;
                                                          
                                                          alert(bar);
                                                          alert(this.foo);
                                                          


                                                          3. Поведение переменной и свойста, и итоговое сохранение в контексте одинаковое (Как видно из премера с дескрипторами — никакой разницы).
                                                          4. Такое объявление:

                                                          "use strict";
                                                          foo = 1;
                                                          


                                                          Это не объявление свойства глобальному объекту или какому либо ещё в прямом смысле. Это объявление верно только тогда, когда используется оператор with.

                                                          var env = {};
                                                          with (env) {
                                                          foo = 1;
                                                          }
                                                          


                                                          Но так как strict mode запрещает использовать with, отключает его. Логично, что и объявление свойства глобальному контексту таким образом отключается (да именно свойства потому, что она работает по алготирму объявления свойства. Объявление переменной работает по другому алгоритму, но в итоге, это тоже свойство объекта).
                                                            0
                                                            Насколько я понимаю, если JS-код выполняется в ходе загрузки страницы, то это трактуется как будто он обернут в (function() {… }()), поэтому «var bar = 1» в таком контексте не объявляет свойство глобального объекта, а объявляет локальную переменную этой псевдо-функции. А вот если этот код исполнить в консоли, то «var bar = 1» и просто «bar = 1» трактуется полностью одинаково — как поле глобального объекта.
                                                              +1
                                                              В консоли выполняется просто eval в глобальном контексте. У eval есть особенность в том, что переменные объявленные внутри него не получают флаг configurable = false и поэтому могут быть удалены. Тот код, котый исполняется между тегами исполняется нормально, внутренней функцие как например в node.js/v8 — runInContext. Это конечно трактовать как первую функцию, например в Chakra в новых ие, это выполнение и есть первая функция. Вся особенность глобального объекта в том, что ключевое слово this этой функции указывает на LexicalEnvironment этой же функции и получается так, что присвоить свойство или объявить переменную этому объекту почти одно и тоже, за исключением, алгоритмов исполнения. В общем виде псевдо код этой функции выглядит так:

                                                              function runInContext(context, code) {
                                                                with (context) {
                                                                  (function() {
                                                                    eval.call(context, code);
                                                                  }).call(context);
                                                                }
                                                              };
                                                              
                                                              function runInStrictContext(context, code) {
                                                                (function() {
                                                                  eval.call(context, code);
                                                                }).call(context);
                                                              };
                                                              


                                                              Конечно оно так не будет работать и это не лучший пример, однако относительно может помочь понять.
                                                              0
                                                              Ещё есть такая штука, ну так, для интереса:

                                                              Object.prototype.globalPropOrVar = 123;
                                                              
                                                              alert(globalPropOrVar);
                                                              
                                                              alert(({}).globalPropOrVar);
                                                              
                                                    0
                                                    А почему так? Объясните, если не трудно. Или глобальные переменные, объявленные c var инициализируются только после того как глобальный скрипт отработал?
                                                      0
                                                      Кажется понял — переменные без var являются свойствами глобального объекта, а с var — глобальными переменными :)

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое