Перейти к содержанию

Области видимости и пространства имён

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

function test() {
  // область видимости
  for (var i = 0; i < 10; i++) {
    // не область видимости
    // считаем
  }
  console.log(i) // 10
}

Замечание: Нотация {...} будет интерпретирована как блочное выражение, а не как литерал объекта, если она не используется в присваивании, операторе return или в качестве функции. Это замечание, вкупе с автоматической расстановкой точек с запятой, может привести к чрезвычайно хитрым ошибкам.

Также JavaScript не знает ничего о различиях в пространствах имён: всё определяется в глобально доступном пространстве имён.

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

Проклятие глобальных переменных

// скрипт A
foo = '42'

// скрипт B
var foo = '42'

Вышеприведённые два скрипта не приводят к одному результату. Скрипт A определяет переменную по имени foo в глобальной области видимости, а скрипт B определяет foo в текущей области видимости.

Повторимся, это вообще не тот же самый эффект. Если вы не используете var — то вы в большой опасности.

// глобальная область видимости
var foo = 42
function test() {
  // локальная область видимости
  foo = 21
}
test()
foo // 21

Из-за того что оператор var опущен внутри функции, фунция test перезапишет значение foo. Это поначалу может показаться не такой уж и большой проблемой, но если у вас имеется тысяча строк JavaScript-кода и вы не используете var, то вам на пути встретятся страшные и трудноотлаживаемые ошибки — и это не шутка.

// глобальная область видимости
var items = [
  /* какой-то список */
]
for (var i = 0; i < 10; i++) {
  subLoop()
}

function subLoop() {
  // область видимости subLoop
  for (i = 0; i < 10; i++) {
    // пропущенный оператор var
    // делаем волшебные вещи!
  }
}

Внешний цикл прекратит работу сразу после первого вызова subLoop, поскольку subLoop перезаписывает глобальное значение переменной i. Использование var во втором цикле for могло бы вас легко избавить от этой ошибки. Никогда не забывайте использовать var, если только влияние на внешнюю область видимости не является тем, что вы намерены получить.

Локальные переменные

Единственный источник локальных переменных в JavaScript - это параметры функций и переменные, объявленные с использованием оператора var.

// глобальная область видимости
var foo = 1
var bar = 2
var i = 2

function test(i) {
  // локальная область видимости для функции test
  i = 5

  var foo = 3
  bar = 4
}
test(10)

В то время как foo и i — локальные переменные в области видимости функции test, присвоение bar переопределит значение одноимённой глобальной переменной.

Всплытие

В JavaScript действует механизм всплытия определения. Это значит, что оба определения с использованием var и определение function будут перенесены наверх заключающей их области видимости.

bar()
var bar = function () {}
var someValue = 42

test()
function test(data) {
  if (false) {
    goo = 1
  } else {
    var goo = 2
  }
  for (var i = 0; i < 100; i++) {
    var e = data[i]
  }
}

Этот код трансформируется ещё перед исполнением. JavaScript перемещает операторы var и определение function наверх ближайшей оборачивающей области видимости.

// выражения с var переместились сюда
var bar, someValue // по умолчанию - 'undefined'

// определение функции тоже переместилось
function test(data) {
  var goo, i, e // потерянная блочная область видимости
  // переместилась сюда
  if (false) {
    goo = 1
  } else {
    goo = 2
  }
  for (i = 0; i < 100; i++) {
    e = data[i]
  }
}

bar() // вылетает с ошибкой TypeError,
// поскольку bar всё ещё 'undefined'
someValue = 42 // присвоения не подвержены всплытию
bar = function () {}

test()

Потерянная область видимости блока не только переместит операторы var вовне циклов и их тел, но и сделает результаты некоторых конструкций с if неинтуитивными.

В исходном коде оператор if изменял глобальную переменную goo, когда, как оказалось, он изменяет локальную переменную — в результате работы всплытия.

Если вы не знакомы со всплытием, то можете посчитать, что нижеприведённый код должен породить ReferenceError.

// проверить, проинициализована ли SomeImportantThing
if (!SomeImportantThing) {
  var SomeImportantThing = {}
}

Но, конечно же, этот код работает: из-за того, что оператор var был перемещён наверх глобальной области видимости

var SomeImportantThing

// другой код может инициализировать здесь переменную SomeImportantThing,
// а может и нет

// убедиться, что она всё ещё здесь
if (!SomeImportantThing) {
  SomeImportantThing = {}
}

Порядок разрешения имён

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

Области видимости функций также содержат внутри себя переменную arguments, которая содержит аргументы, переданные в функцию.

Например, когда JavaScript пытается получить доступ к переменной foo в области видимости функции, он будет искать её по имени в такой последовательности:

  1. Если в текущей области видимости есть выражение var foo, использовать его.
  2. Если один из параметров функции называется foo, использовать его.
  3. Если функция сама называется foo, использовать её.
  4. Перейти на одну область видимости выше и начать с п. 1

Замечание: Наличие параметра функции с именем arguments не позволит движку создать объект arguments, создающийся по умолчанию.

Пространства имён

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

;(function () {
  // самостоятельно созданное "пространство имён"

  window.foo = function () {
    // открытое замыкание
  }
})() // сразу же выполнить функцию

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

;// разобрать функцию внутри скобок
(function () {})() // и вернуть объект функции // вызвать результат разбора

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

// Два других способа
;+(function () {})()
;(function () {})()

Заключение

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

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