Javascript область видимости переменных

У JS есть несколько концепций, связанных с областью видимости (scope), которые не всегда ясны начинающим разработчикам (и иногда даже опытным). Эта статья посвящена тем, кто стремится погрузиться в пучину областей видимости JS, услышав такие слова, как область видимости, замыкание, “this”, область имён, область видимости функции, глобальные переменные, лексическая область видимости, приватные и публичные области… Надеюсь, по прочтению материала вы сможете ответить на следующие вопросы:

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

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

В 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
  }
})();

Перевод: http://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/

Еще на тему области видимости Javascript

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

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

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

Также можно было бы упямонуть про ES6 let

Javascript область видимости переменных: 1 комментарий

  1. То же самое касается и второго примера. Как только div и его обработчик станут недоступны из roots, они оба будут уничтожены сборщиком мусора, несмотря на наличие циклических ссылок друг на друга.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *