Области видимости в JavaScript +22


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


Картинка по запросу «области видимости». Извините, если вызвали приступ ностальгии )

Элементарное определение области видимости выглядит так: это область, где компилятор ищет переменные и функции, когда они ему нужны. Думаете, что звучит это слишком просто? Предлагаем разобраться вместе.

Интерпретатор JavaScript


Прежде чем говорить об областях видимости, нужно обсудить интерпретатор JavaScript, рассмотреть то, как он воздействует на различные области видимости. При исполнении JS-кода интерпретатор проходится по нему дважды.

Первый проход по коду, называемый ещё проходом компиляции — это то, что наиболее сильно воздействует на области видимости. Интерпретатор просматривает код в поисках объявлений переменных и функций и поднимает эти объявления в верхнюю часть текущей области видимости. Важно отметить, что поднимаются только объявления переменных, а операции присвоения остаются как есть — для следующего прохода, называемого проходом исполнения.

Для того, чтобы лучше это понять, рассмотрим простой фрагмент кода:

'use strict'

var foo = 'foo';
var wow = 'wow';

function bar (wow) {
  var pow = 'pow';
  console.log(foo); // 'foo'
  console.log(wow); // 'zoom'
}

bar('zoom');
console.log(pow); // ReferenceError: pow is not defined

Этот код, после компиляции, будет выглядеть примерно так:

'use strict'
// Переменные подняты в верхнюю часть текущей области видимости
var foo;
var wow;

// Объявления функций подняты целиком, вместе с присвоением, в верхнюю часть текущей области видимости
function bar (wow) {
  var pow;
  pow = 'pow';
  console.log(foo);
  console.log(wow);
}

foo = 'foo';
wow = 'wow';

bar('zoom');
console.log(pow); // ReferenceError: pow is not defined

Здесь надо обратить внимание на то, что объявления поднимаются в верхнюю часть их текущей области видимости. Это, как будет видно ниже, очень важно для понимания областей видимости в JavaScript.

Например, переменная pow была объявлена в функции bar, так как это — её область видимости. Обратите внимание на то, что переменная объявлена не в родительской, по отношению к функции, области видимости.

Параметр wow функции bar так же объявлен в области видимости функции. На самом деле, все параметры функции неявно объявлены в её области видимости, и именно поэтому команда console.log(wow) в девятой строке, внутри функции, выводит zoom вместо wow.

Лексическая область видимости


Рассмотрев особенности работы интерпретатора JavaScript и затронув тему поднятия переменных и функций, мы можем перейти к разговору об областях видимости. Начнём с лексической области видимости. Можно сказать, что это — область видимости, которая формируется во время компиляции. Другими словами, решение о границах этой области видимости принимается во время компиляции. Для целей этой статьи мы проигнорируем исключения из этого правила, которые возникают в коде, который использует команды eval или with. Полагаем, что эти команды, в любом случае, использовать не стоит.

Второй проход интерпретатора — это тот, в ходе которого выполняется присвоение значений переменным и исполняются функции. В вышеприведённом примере кода именно во время этого прохода выполняется вызов bar() в строке 12.

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

Если мы посмотрим на строку 8, где находится команда console.log(foo);, интерпретатору, прежде чем исполнить эту команду, понадобится найти объявление foo. Первое, что он делает, опять же, ищет в текущей области видимости, которой в этот момент является область видимости функции bar, а не глобальная область видимости. Объявлена ли переменная foo в области видимости функции? Нет, это не так. Затем он переходит на уровень вверх, к родительской области видимости, и ищет объявление переменной там. Область видимости, в которой объявлена функция — это глобальная область видимости. Объявлена ли переменная foo в глобальной области видимости? Да, это так. Поэтому интерпретатор может взять значение переменной и исполнить команду.

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

Если того, что ищет интерпретатор, нет и в глобальной области видимости, он выдаст ошибку ReferenceError.

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

'use strict'

var foo = 'foo';

function bar () {
  var foo = 'bar';
  console.log(foo);
}

bar();

Этот код выведет в консоль строку bar, а не foo, так как объявление переменной foo в шестой строке перекроет объявление переменной с таким же именем в третьей строке.

Затенение переменных — это шаблон проектирования, который может быть полезен в том случае, если нужно замаскировать некоторые переменные и предотвратить доступ к ним из конкретных областей видимости. Надо сказать, что я обычно избегаю использования этого приёма, применяя его только если без него совершенно невозможно обойтись, так как я уверен в том, что использование одинаковых имён переменных ведёт к путанице при командной разработке. Использование затенения способно привести к тому, что разработчик может решить, что в переменной хранится не то, что в ней на самом деле есть.

Функциональная область видимости


Как мы видели, рассматривая лексическую область видимости, интерпретатор объявляет переменные в текущей области видимости, что означает, что переменные, объявленные в функции, объявлены в функциональной области видимости. Эта область видимости ограничена самой функцией и её потомками — другими функциями, объявленными внутри этой функции.

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

'use strict'

function convert (amount) {
   var _conversionRate = 2; // Доступно только в функциональной области видимости
   return amount * _conversionRate;
}

console.log(convert(5));
console.log(_conversionRate); // ReferenceError: _conversionRate is not defined

Блочная область видимости


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

В ES3 выражение catch в конструкции try / catch имеет блочную область видимости, что означает, что у этого выражения есть собственная область видимости. Важно отметить, что выражение try не имеет блочной области видимости, она есть только у выражения catch. Рассмотрим пример:

'use strict'

try {
  var foo = 'foo';
  console.log(bar);
}
catch (err) {
  console.log('In catch block');
  console.log(err);
}

console.log(foo);
console.log(err);

Этот код выдаст ошибку на пятой строке, когда мы попытаемся получить доступ к bar, что приведёт к тому, что интерпретатор перейдёт к выражению catch. В области видимости выражения объявлена переменная err, которая не будет доступна извне. На самом деле, ошибка будет выдана, когда мы попытаемся вывести в лог значение переменной err в строке console.log(err);. Вот что выведет этот код:

In catch block
ReferenceError: bar is not defined
    (...Error stack here...)
foo
ReferenceError: err is not defined
(...Error stack here...)

Обратите внимание на то, что переменная foo доступна за пределами конструкции try / catch, а err — нет.

Если говорить о ES6, то при использовании ключевых слов let и const переменные и константы неявно присоединяются к текущей блочной области видимости вместо функциональной области видимости. Это означает, что эти конструкции ограничены блоком, в котором они используются, будет ли это блок if, блок for, или функция. Вот пример, который поможет лучше это понять:

'use strict'

let condition = true;

function bar () {
  if (condition) {
    var firstName = 'John'; // Доступно во всей функции
let lastName = 'Doe'; // Доступно только в блоке if
    const fullName = firstName + ' ' + lastName; // Доступно только в блоке if
  }

  console.log(firstName); // John
  console.log(lastName); // ReferenceError
  console.log(fullName); // ReferenceError
}

bar();

Ключевые слова let и const позволяют нам использовать принцип наименьшего раскрытия (principle of least disclosure). Следование этому принципу означает, что переменная должна быть доступна в наименьшей из возможных областей видимости. До ES6 разработчики часто добивались эффекта блочной области видимости, пользуясь стилистическим приёмом объявления переменных с ключевым словом var в немедленно исполняемом функциональном выражении (Immediately Invoked Function Expression, IIFE), но теперь, благодаря let и const, можно применить функциональный подход. Некоторые из основных преимуществ этого принципа заключаются в избежании нежелательного доступа к переменным, и, таким образом, снижении вероятности ошибок. Кроме того, это позволяет сборщику мусора освобождать память от ненужных переменных при выходе из блочной области видимости.

Немедленно исполняемые функциональные выражения


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

'use strict'

var foo = 'foo';

(function bar () {
  console.log('in function bar');
})()

console.log(foo);

Этот код выведет строку in function bar до вывода foo, так как функция bar исполняется немедленно, без необходимости явно вызывать её, используя конструкцию вида bar(). Это происходит по следующим причинам:

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

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

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

'use strict'

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log('index: ' + i);
  }, 1000);
}

Вполне можно ожидать, что этот код выведет 0, 1, 2, 3, 4. Однако, реальный результат выполнения данного цикла for, в котором вызывается асинхронная операция setTimeout, будет выглядеть так:

index: 5
index: 5
index: 5
index: 5
index: 5

Причина этого в том, что к тому времени, как истечёт 1000 миллисекунд, выполнение цикла for завершится и счётчик i окажется равным 5.

Для того, чтобы код работал так, как ожидается, выводил последовательность чисел от 0 до 4, нам нужно использовать IIFE для сохранения необходимой нам области видимости:

'use strict'

for (var i = 0; i < 5; i++) {
  (function logIndex(index) {
    setTimeout(function () {
      console.log('index: ' + index);
    }, 1000);
  })(i)
}

В этом примере мы передаём значение i в IIFE. У функционального выражения будет собственная область видимости, на которую то, что происходит в цикле for, уже не подействует. Вот что выведет этот код:

index: 0
index: 1
index: 2
index: 3
index: 4

Итоги


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

Надеюсь, этот рассказ помог вам лучше понять области видимости в JavaScript, а значит, улучшить качество ваших программ. Также можем порекомендовать для прочтения эту публикацию на Хабре.

Уважаемые JS-разработчики! Просим вас поделиться интересными приёмами работы с областями видимости в JavaScript.




К сожалению, не доступен сервер mySQL