Всё, что вы хотели знать об областях видимости в JavaScript (но боялись спросить)

http://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/
  • Перевод
У JS есть несколько концепций, связанных с областью видимости (scope), которые не всегда ясны начинающим разработчикам (и иногда даже опытным). Эта статья посвящена тем, кто стремится погрузиться в пучину областей видимости JS, услышав такие слова, как область видимости, замыкание, “this”, область имён, область видимости функции, глобальные переменные, лексическая область видимости, приватные и публичные области… Надеюсь, по прочтению материала вы сможете ответить на следующие вопросы:

— что такое область видимости?
— что есть глобальная/локальная ОВ?
— что есть пространство имён и чем оно отличается от ОВ?
— что обозначает ключевое слово this, и как оно относится с ОВ?
— что такое функциональная и лексическая ОВ?
— что такое замыкание?
— как мне всё это понять и сотворить?

Что такое область видимости?



В JS область видимости – это текущий контекст в коде. ОВ могут быть определены локально или глобально. Ключ к написанию пуленепробиваемого кода – понимание ОВ. Давайте разбираться, где переменные и функции доступны, как менять контекст в коде и писать более быстрый и поддерживаемый код (который и отлаживать быстрее). Разбираться с ОВ просто – задаём себе вопрос, в какой из ОВ мы сейчас находимся, в А или в Б?

Что есть глобальная/локальная ОВ?



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

// глобальная ОВ
var name = 'Todd';


Глобальная ОВ – ваш лучший друг и худший кошмар. Обучаясь работе с разными ОВ, вы не встретите проблем с глобальной ОВ, разве что вы увидите пересечения имён. Часто можно услышать «глобальная ОВ – это плохо», но нечасто можно получить объяснение – почему. ГОВ – не плохо, вам нужно её использовать при создании модулей и API, которые будут доступны из разных ОВ, просто нужно использовать её на пользу и аккуратно.

Все мы использовали jQuery. Как только мы пишем

jQuery('.myClass');


мы получаем доступ к jQuery в глобальной ОВ, и мы можем назвать этот доступ пространством имён. Иногда термин «пространство имён» используют вместо термина ОВ, однако обычно им обозначают ОВ самого уровня. В нашем случае jQuery находится в глобальной ОВ, и является нашим пространством имён. Пространство имён jQuery определено в глобальной ОВ, которая работает как ПИ для библиотеки jQuery, в то время как всё её содержимое наследуется от этого ПИ.

Что такое локальная ОВ?



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

Если я определю функции и задам внутри переменные, они принадлежат локальной ОВ. Пример:

// ОВ A: глобальная
var myFunction = function () {
  // ОВ B: локальная
};


Все переменные из ЛОВ не видны в ГОВ. К ним нельзя получить доступ снаружи напрямую. Пример:

var myFunction = function () {
  var name = 'Todd';
  console.log(name); // Todd
};
// ReferenceError: name is not defined
console.log(name);


Переменная “name” относится к локальной ОВ, она не видна снаружи и поэтому не определена.

Функциональная ОВ.



Все локальные ОВ создаются только в функциональных ОВ, они не создаются циклами типа for или while или директивами типа if или switch. Новая функция – новая область видимости. Пример:

// ОВ A
var myFunction = function () {
  // ОВ B
  var myOtherFunction = function () {
    // ОВ C
  };
};


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

Лексическая ОВ



Если одна функция определена внутри другой, внутренняя имеет доступ к ОВ внешней. Это называется «лексической ОВ», или «замыканием», или ещё «статической ОВ».

var myFunction = function () {
  var name = 'Todd';
  var myOtherFunction = function () {
    console.log('My name is ' + name);
  };
  console.log(name);
  myOtherFunction(); // вызов функции
};

// Выводит:
// `Todd`
// `My name is Todd`


С лексической ОВ довольно просто работать – всё, что определено в ОВ родителя, доступно в ОВ ребенка. К примеру:

var name = 'Todd';
var scope1 = function () {
  // name доступно здесь
  var scope2 = function () {
    // name и здесь
    var scope3 = function () {
      // name и даже здесь!
    };
  };
};


В обратную сторону это не работает:

// name = undefined
var scope1 = function () {
  // name = undefined
  var scope2 = function () {
    // name = undefined
    var scope3 = function () {
      var name = 'Todd'; // локальная ОВ
    };
  };
};


Всегда можно вернуть ссылку на “name”, но не саму переменную.

Последовательности ОВ



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

Замыкания



Живут в тесном союзе с лексическими ОВ. Хорошим примером использования является возврат ссылки на функцию. Мы можем возвращать наружу разные ссылки, которые делают возможным доступ к тому, что было определено внутри.

var sayHello = function (name) {
  var text = 'Hello, ' + name;
  return function () {
    console.log(text);
  };
};


Чтобы вывести на экран текст, недостаточно просто вызвать функцию sayHello:

sayHello('Todd'); // тишина


Функция возвращает функцию, поэтому её надо сначала присвоить, а потом вызвать:

var helloTodd = sayHello('Todd');
helloTodd(); // вызывает замыкание и выводит 'Hello, Todd'


Можно конечно вызвать замыкание и напрямую:

sayHello('Bob')(); // вызывает замыкание без присваивания


В AngularJS используются подобные вызовы в методеs $compile, где нужно передавать ссылку на текущую ОВ:

$compile(template)(scope);


Можно догадаться, что упрощённо их код выглядит примерно так:

var $compile = function (template) {
  // всякая магия
  // без доступа к scope
  return function (scope) {
    // здесь есть доступ и к `template` и к `scope`
  };
};


Функция не обязана ничего возвращать, чтобы быть замыканием. Любой доступ к переменным извне текущей ОВ создаёт замыкание.

ОВ и 'this'



Каждая ОВ назначает своё значение переменной “this”, в зависимости от способа вызова функции. Мы все использовали ключевое слово this, но не все понимают, как оно работает и какие есть отличия при вызовах. По умолчанию, оно относится к объекту самой внешней ОВ, текущему окну. Пример того, как разные вызовы меняют значения this:

var myFunction = function () {
  console.log(this); // this = глобальное, [объект Window]
};
myFunction();

var myObject = {};
myObject.myMethod = function () {
  console.log(this); // this = текущий объект { myObject }
};

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  console.log(this); // this = элемент <nav> 
};
nav.addEventListener('click', toggleNav, false);


Встречаются и проблемы со значением this. В следующем примере внутри одной и той же функции значение и ОВ могут меняться:

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  console.log(this); // <nav> element
  setTimeout(function () {
    console.log(this); // [объект Window]
  }, 1000);
};
nav.addEventListener('click', toggleNav, false);


Здесь мы создали новую ОВ, которая вызывается не из обработчика событий, а значит, относится к объекту window. Можно, например, запоминать значение this в другой переменной, чтобы не возникало путаницы:

var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
  var that = this;
  console.log(that); // элемент <nav> 
  setTimeout(function () {
    console.log(that); // элемент <nav> 
  }, 1000);
};
nav.addEventListener('click', toggleNav, false);


Меняем ОВ при помощи .call(), .apply() и .bind()



Иногда есть необходимость менять ОВ в зависимости от того, что вам нужно.
В примере:

var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
  console.log(this); // [объект Window]
}


Значение this не относится к перебираемым элементам, мы ничего не вызываем и не меняем ОВ. Давайте посмотрим, как мы можем менять ОВ (точнее, мы меняем контекст вызова функций).

.call() and .apply()



Методы .call() и .apply() позволяют передавать ОВ в функцию:

var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
  (function () {
    console.log(this);
  }).call(links[i]);
}


В результате в this передаются значения перебираемых элементов. Метод .call(scope, arg1, arg2, arg3) принимает список аргументов, разделённых запятыми, а метод .apply(scope, [arg1, arg2]) принимает массив аргументов.

Важно помнить, что методы .call() или .apply() вызывают функции, поэтому вместо

myFunction(); // вызывает myFunction


позвольте .call() вызвать функцию и передать параметр:

myFunction.call(scope);


.bind()



.bind() не вызывает функцию, а просто привязывает значения переменных перед её вызовом. Как вы знаете, мы не можем передавать параметры в ссылки на функции:

// работает
nav.addEventListener('click', toggleNav, false);

// приводит к немедленному вызову функции
nav.addEventListener('click', toggleNav(arg1, arg2), false);


Это можно исправить, создав новую вложенную функцию:

nav.addEventListener('click', function () {
  toggleNav(arg1, arg2);
}, false);


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

nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);

Приватные и публичные ОВ



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

(function () {
  // здесь приватная ОВ
})();


Добавим функциональности:

(function () {
  var myFunction = function () {
    // делаем здесь, что нужно
  };
})();


Но вызвать эту функцию напрямую нельзя:

(function () {
  var myFunction = function () {
    // делаем здесь, что нужно
  };
})();
myFunction(); // Uncaught ReferenceError: myFunction is not defined


Вот вам и приватная ОВ. Если вам нужна публичная ОВ, воспользуемся следующим трюком. Создаём пространство имён Module, которое содержит всё, относящееся к данному модулю:

// определяем модуль
var Module = (function () {
  return {
    myMethod: function () {
      console.log('myMethod has been called.');
    }
  };
})();

// вызов методов модуля
Module.myMethod();


Директива return возвращает методы, доступные публично, в глобальной ОВ. При этом они относятся к нужному пространству имён. Модуль Module может содержать столько методов, сколько нужно.

// определяем модуль
var Module = (function () {
  return {
    myMethod: function () {

    },
    someOtherMethod: function () {

    }
  };
})();

// вызов методов модуля
Module.myMethod();
Module.someOtherMethod();


Не нужно стараться вываливать все методы в глобальную ОВ и загрязнять её. Вот так можно организовать приватную ОВ, не возвращая функции:

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {

    }
  };
})();


Мы можем вызвать publicMethod, но не можем privateMethod – он относится к приватной ОВ. В эти функции можно засунуть всё что угодно — addClass, removeClass, вызовы Ajax/XHR, Array, Object, и т.п.

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

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {
      // есть доступ к методу `privateMethod`:
      // privateMethod();
    }
  };
})();


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

Пример возврата объекта с использованием приватных и публичных методов:

var Module = (function () {
  var myModule = {};
  var privateMethod = function () {

  };
  myModule.publicMethod = function () {

  };
  myModule.anotherPublicMethod = function () {

  };
  return myModule; // returns the Object with public methods
})();

// использование
Module.publicMethod();


Удобно начинать название приватных методов с подчёркивания, чтобы визуально отличать их от публичных:

var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
})();


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

var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
  return {
    publicMethod: publicMethod,
    anotherPublicMethod: anotherPublicMethod
  }
})();
Поделиться публикацией
Комментарии 38
    +6
    Очень неплохая статья, так держать!

    Можно было бы еще добавить определение хойстинга/hoisting, т.к. оно также играет важную роль. Так, например, что будет выведено в консоль в след.ситуации:

    var func = function() { console.log('1'); };
    function func() { console.log('2'); }
    func();
    


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

    Также можно было бы упямонуть про ES6 let, которая напрямую затрагивает тему данной статьи.
    +3
    Хорошая статья для начинающих, но странно, что в конце 2013 года автор совсем не затронул let и его влияние на области видимости.

    UPD: Черт, выше уже написали про let.
      +4
      Рано его еще использовать. В частности v8 еще не умеет оптимизировать функции с let (возможно ситуация изменится с запуском TurboFan). Что влечет в свою очередь примерно x20 кратную потерю производительности, что порой критично. Да и вообще меня не покидает ощущение, что классы и let — это эдакий популизм. В отличии от реально нужных генераторов и ES7 async/await, argument pattern'ов и destructuring assignment (который, к сожалению, в v8 еще не родили, хотя использовать его с генераторами самое оно).
      0
      Понравилось, спасибо. Вроде и ничего нового, но зато всё по делу.
      Хотя у меня на практике обычно по простому
      var Module= {
          mothod1 : function() {
      	
          },
          mothod2 : function() {
      		
          },
          mothod3 : function() {
      		
          }
      }
        –1
        Отличная статья, но есть неточность: в первом примере — это не всегда так.

        В ноде это будет объявлено в глобальной области, однако в браузере — не в глобальной, а внутри области window.
        Глобальная область в браузере доступна примерно так: name = 'Todo';
        var name = 'Todo'; // Локальная ОВ
        globalName = 'Todo'; // Глобальная ОВ (покуда без var)
        

        Не могу придумать пример, когда это важно, но раз уж статья об областях видимости…
        Неверующим
        console.log(this); // > Window
          0
          хм,
          mySuperVar = 'text';
          typeof(window.mySuperVar); //string
          window.mySuperVar; //text
          

          но…
          console.log(window.mySuperVar); //undefined
          
            0
            В браузере? Будет text:

            mySuperVar = 'text';
            typeof(window.mySuperVar); //string
            window.mySuperVar; //text
            console.log(window.mySuperVar); //undefined
            // actual result: text 
            
              0
              да-да, я об этом и говорю
                0
                Я думаю, имеется ввиду, что если это писать в консоли, то вывод будет примерно такой:
                var myVar = "text";
                console.log(window.myVar);
                > undefined
                > "text"
                

                Покуда сам метод console.log() ничего не возвращает, то сначала печатается undefined, а потом уже переменная.
                  0
                  да-да, основной посыл был в том, что и без var оно в window попадает…
                    0
                    переменная всегда попадает в тек.контекст, соотв. не важно с var или без, она будет доступна из window:

                    var x = 'xxx';
                    y = 'yyy';
                    console.log(window.x); // 'xxx'
                    console.log(window.y); // 'yyy'
                    
                      0
                      да что ж такое)) в своём примере именно это я и хотел сказать)
                    +1
                    Как раз таки наоборот:

                    var myVar = "text";
                    console.log(window.myVar);
                    > text
                    > undefined
                    
                0
                alert(window.mySuperVar)
                

                выводит text
                  0
                  более того — console.dir(window.mySuperVar);
                  выведет сначала «text», а потом уж undefined
                    0
                    Я сначала не понял, о чём вы. Я подумал, что console.log(window.mySuperVar) выводит undefined

                    на самом деле undefined — это не вывод, а возврат функции console.log
                      +1
                      ага, тут вообще ветка получилась сродни строительству Вавилонской башни)
                      +1
                      function a() {
                        console.log(1);
                        return 2;
                      }
                      a();
                      


                      Вывод:
                      1 2

                      здесь уже нет никакого undefined. Здесь 1 — вывод console.log(1), а 2 — возврат функции a
                  0
                  В обеих ситуациях это считается глобальной областью: this в браузере это window, в nodejs — переменная определяемая системой.
                    –2
                    Этот загадочный js…

                    Тут подумал — если мы используем iframe, то до него можно достучаться через window.frames[0].variable; Следовательно, в iframe это все-таки будет локальный контекст по сравнению с родительским. Хотя даже без var он наружу не бросает. Хитрый js однако. К слову, в статье про области видимости как раз это можно было и раскрыть.

                    ОФФТОП. (Да, поведу себя как ребенок, но раньше об этом не задумывался)
                    Тут меня осенил гениальный план, как можно воровать личные данные: делаете на своем сайте iframe на чужой сайт, он там открывается, а мы имеем доступ к нему. Хм, я подумал — слишком простая и очевидная схема, и правда — vk так не открылся. Но не сложно догадаться, что виной этому хедер — X-iframe-options: deny. Полез лазить по интернету. Крупные сайты предусмотрели этот заголовок, но из более мелких (всякие интернет-магазины самописные, а так же всякие самописные бложики) спокойно открываются через iframe почти все. Браузер safari. Хм, печально, товарищи.
                      0
                      я может не понял, но чем это отличается от window.variable непосредственно на таргет сайте?
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Хм. Это все хорошо. И это логично. Но попробовал у себя на локальном файле через file:// — и, к моему удивлению, сработало. У меня был полный доступ к открывшейся странице. Удивленный, полез воспроизводить это на jsfiddle. Но там не сработало. Странное поведение.
                            0
                            Хм. В Safari 8.0 этот файл отрабатывает без проблем, если его открывать как file://. На мой взгляд, это может быть достаточно большой дыркой.
                            Скрытый текст
                            <!DOCTYPE html>
                            <html>
                            <head>
                            </head>
                            <script>
                            var k = 3;
                            l = 5;
                            var load = function()
                            {
                             window.frames[0].$('body').click(function() { alert('Алерт'); });
                            }
                            </script>
                            
                            <body>
                            <iframe onload="load()" src="http://raal100.narod.ru" width='500' height='500'></iframe>
                            </body></html>
                            

                          0
                          вот так надо:

                          window.frames[0].contentWindow.variable
                          


                          просматривайте, какими свойствами обладает объект, в данном случае объект window.frames[0], и да пребудет с вами сила

                          И ещё: во фрейме должен быть загружен страница того же домена или поддомена.

                          если во фрейме загружен поддомен, то в нём должно быть запущена строчка document.domain = "<домен родительского окна>".
                      +7
                      Каждая ОВ назначает своё значение переменной “this”

                      Это не совсем верно. this это свойство контекста выполнения (execution context), а не области видимости (scope).
                      например,
                      var b = 1;
                      function a() {
                          console.log('scope b:', b);
                          console.log('this b:', this.b);
                          // здесь небольшая магия, в es6, если не указано явно, this будет null,
                          // в es5 - глобальный объект
                      }
                      a();
                      a.call({b:2});
                      
                        +8
                        что обозначает ключевое слово this, и как оно относится с ОВ?


                        Никак. Кроме того, что они используются в одном языке. Автор оригинальной статьи или всё переупрощает, или заблуждается.

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

                        Рассмотрим такой код:

                        (function(){
                          var that = 42;
                          with({
                            "that": "foo",
                            "this": "bar"
                          }){
                            console.log(that);
                            console.log(this);
                          }
                        })();
                        


                        Первый консоль-лог выведет «foo». Переменная that, конечно, объявлена и равна 42, но это во внешней ОВ. Во внутренней есть своя переменная that, и она затеняет («shadowing») переменную из внешней ОВ. Несмотря на экзотический оператор, поведение вполне привычное.

                        А вот второй консоль-лог выведет глобальный объект. Во внутренней ОВ есть переменная под именем «this», но она не затенила this. Единственное логичное объяснение — при разрешении значения this механизм ОВ не используется. А сам this — это ключевое слово, а не переменная, и ни в какой области видимости переменных не находится, точно так же, как и ключевые слова null, true или false.

                        Ещё раз уточню, это упрёк автору статьи, а не переводчику. Переводчику только похвала, за хороший перевод.
                          0
                          Поддерживаю. Согласен что this это не переменная. Это ссылка на саму локальную область видимости. Почему нельзя так сказать?
                            0
                            Это ссылка на саму локальную область видимости. Почему нельзя так сказать?

                            да потому что this только по умолчанию указывает на локальную область видимости, и this можно менять вызовами apply, call, bind
                              0
                              Да нет же, this вообще не указывает на локальную область видимости. Вот, товарищ m1el даже уже продемонстрировал это.

                              Видимо, вас сбила с толку фраза «Каждая ОВ назначает своё значение переменной “this”,..»
                              В оригинале она гласит: «Each scope binds a different value of this,..»
                              Т.е, «Каждая… своё значение» тут в смысле «каждый раз разное значение».
                          +1
                          Статья неплохая для начинающих, но не полная, либо автор сам не знает, либо не удосужился написать про то, что значения переменных в функциях при ее выполнении инициализируются сразу же как undefined не зависимо есть ли эти переменные в глобальной области или родительской.
                            0
                            Ох уж эти переводы. Почему обязательно «переводить» название языка как яваскрипт, но при этом писать this, call, return, а не это, вызов, вернуть?
                              +1
                              потому что this и call, return — зарезервированные слова. И как бы понимать, что что имелось в виду под словом «вызов» — call или apply трудновато.
                              +3
                              (ворчу) Вот начитаются сейчас про scope, наизучают js, а потом начинают писать подобное:

                              [].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})
                                +2
                                Есть ощущение, что автор не программировал с применением ООП, и инкапсуляцию описывает как чудо света.

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


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

                                Но вообще достаточно неплохая статья, одна из лучших на тему.

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

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