- Всё, что вы хотели знать об областях видимости в JavaScript (но боялись спросить)
- Что такое область видимости?
- Что есть глобальная/локальная ОВ?
- Что такое локальная ОВ?
- Функциональная ОВ.
- Лексическая ОВ
- Последовательности ОВ
- Замыкания
- ОВ и ‘this’
- Меняем ОВ при помощи .call(), .apply() и .bind()
- .call() and .apply()
- .bind()
- Приватные и публичные ОВ
Всё, что вы хотели знать об областях видимости в JavaScript (но боялись спросить)
У JS есть несколько концепций, связанных с областью видимости (scope), которые не всегда ясны начинающим разработчикам (и иногда даже опытным). Эта статья посвящена тем, кто стремится погрузиться в пучину областей видимости JS, услышав такие слова, как область видимости, замыкание, “this”, область имён, область видимости функции, глобальные переменные, лексическая область видимости, приватные и публичные области… Надеюсь, по прочтению материала вы сможете ответить на следующие вопросы:
— что такое область видимости?
— что есть глобальная/локальная ОВ?
— что есть пространство имён и чем оно отличается от ОВ?
— что обозначает ключевое слово this, и как оно относится с ОВ?
— что такое функциональная и лексическая ОВ?
— что такое замыкание?
— как мне всё это понять и сотворить?
Что такое область видимости?
В JS область видимости – это текущий контекст в коде. ОВ могут быть определены локально или глобально. Ключ к написанию пуленепробиваемого кода – понимание ОВ. Давайте разбираться, где переменные и функции доступны, как менять контекст в коде и писать более быстрый и поддерживаемый код (который и отлаживать быстрее). Разбираться с ОВ просто – задаём себе вопрос, в какой из ОВ мы сейчас находимся, в А или в Б?
Что есть глобальная/локальная ОВ?
Не написав ни строчки кода, мы уже находимся в глобальной ОВ. Если мы сразу определяем переменную, она находится в глобальной ОВ.
// глобальная ОВ var name = 'Todd';
Глобальная ОВ – ваш лучший друг и худший кошмар. Обучаясь работе с разными ОВ, вы не встретите проблем с глобальной ОВ, разве что вы увидите пересечения имён. Часто можно услышать «глобальная ОВ – это плохо», но нечасто можно получить объяснение – почему. ГОВ – не плохо, вам нужно её использовать при создании модулей и API, которые будут доступны из разных ОВ, просто нужно использовать её на пользу и аккуратно.
Все мы использовали jQuery. Как только мы пишем
мы получаем доступ к 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:
Функция возвращает функцию, поэтому её надо сначала присвоить, а потом вызвать:
var helloTodd = sayHello('Todd'); helloTodd(); // вызывает замыкание и выводит 'Hello, Todd'
Можно конечно вызвать замыкание и напрямую:
sayHello('Bob')(); // вызывает замыкание без присваивания
В AngularJS используются подобные вызовы в методеs $compile, где нужно передавать ссылку на текущую ОВ:
Можно догадаться, что упрощённо их код выглядит примерно так:
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'); // var toggleNav = function () < console.log(this); // this = элемент >; nav.addEventListener('click', toggleNav, false);
Встречаются и проблемы со значением this. В следующем примере внутри одной и той же функции значение и ОВ могут меняться:
var nav = document.querySelector('.nav'); // var toggleNav = function () < console.log(this); // element setTimeout(function () < console.log(this); // [объект Window] >, 1000); >; nav.addEventListener('click', toggleNav, false);
Здесь мы создали новую ОВ, которая вызывается не из обработчика событий, а значит, относится к объекту window. Можно, например, запоминать значение this в другой переменной, чтобы не возникало путаницы:
var nav = document.querySelector('.nav'); // var toggleNav = function () < var that = this; console.log(that); // элемент setTimeout(function () < console.log(that); // элемент >, 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() вызвать функцию и передать параметр:
.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 () < 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 >>)();