Области видимости и пространства имён¶
Хотя 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
в области видимости функции, он будет искать её по имени в такой последовательности:
- Если в текущей области видимости есть выражение
var foo
, использовать его. - Если один из параметров функции называется
foo
, использовать его. - Если функция сама называется
foo
, использовать её. - Перейти на одну область видимости выше и начать с п. 1
Замечание: Наличие параметра функции с именем
arguments
не позволит движку создать объектarguments
, создающийся по умолчанию.
Пространства имён¶
Нередкое последствие наличия только одного глобального пространства имён — проблема с перекрытием имён переменных. В JavaScript эту проблему легко избежать, используя анонимные обёртки.
;(function () {
// самостоятельно созданное "пространство имён"
window.foo = function () {
// открытое замыкание
}
})() // сразу же выполнить функцию
Безымянные функции являются выражениями; поэтому, чтобы вы имели возможность их выполнить, они сперва должны быть разобраны.
// разобрать функцию внутри скобок
;(function () {})() // и вернуть объект функции // вызвать результат разбора
Есть другие способы разбора и последующего вызова выражения с функцией; они, хоть и различаются в синтаксисе, но действуют одинаково.
// Два других способа
;+(function () {})()
;(function () {})()
Заключение¶
Рекомендуется всегда использовать анонимную обёртку для заключения кода в его собственное пространство имён. Это не только защищает код от совпадений имён, но и позволяет создавать более модульные программы.
Важно добавить, что использование глобальных переменных считается плохой практикой. Любое их использование демонстрирует плохое качество кода и может привести к трудноуловимым ошибкам.