Pull to refresh

Область видимости в JavaScript и «поднятие» переменных и объявлений функций

Reading time 6 min
Views 157K
Original author: Ben Cherry
Вы знаете, какое значение выведет этот код на JavaScript?
var foo = 1; 
function bar() { 
    if (!foo) { 
        var foo = 10; 
    } 
    alert(foo); 
} 
bar();


Если вас удивляет, что выведется «10», то следующий код вас и вовсе запутает:
var a = 1; 
function b() { 
    a = 10; 
    return; 
    function a() {} 
} 
b(); 
alert(a);

В этом случае браузер выведет «1». Так что, собственно, происходит? Хотя такое поведение кажется странным, опасным и сбивающим с толку, на самом деле это очень мощное и выразительное средство JavaScript. Я не знаю, есть ли официальное название для такого поведения, но мне нравится использовать термин «поднятие»(«hoisting»). В этой статье я попытаюсь пролить свет на этот механизм языка, но сначала давайте поговорим об области видимости в JavaScript.

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


Одна из причин, приводящих в замешательство новичков, — это область видимости. Вообще, не только новичков. Я встречал много опытных JavaScript-разработчиков, которые не понимают механизм области видимости в JavaScript. Причина в том, что внешне JavaScript очень похож на любой другой Си-подобный язык.
Давайте рассмотрим следующий код на Cи:
#include <stdio.h> 
int main() { 
    int x = 1; 
    printf("%d, ", x); // 1 
    if (1) { 
        int x = 2; 
        printf("%d, ", x); // 2 
    } 
    printf("%d\n", x); // 1 
}


Эта программа выведет 1, 2, 1, потому что Си и все остальные Си-подобные языки реализуют области видимости на уровне блоков кода. Когда исполняется новый блок кода, например условие if, новые переменные, объявленные в нём, не повлияют на переменные внешней области видимости.
Но не в случае JavaScript. Попробуйте запустить вот этот код в Firebug:
var x = 1; 
console.log(x); // 1 
if (true) { 
    var x = 2; 
    console.log(x); // 2 
} 
console.log(x); // 2

На этот раз будут выведены числа 1, 2, 2. Это связано с тем, что в JavaScript используется область видимости на уровне функций. Это совсем не то, что мы привыкли видеть в языках программирования, вроде Си. Блоки кода, вроде того, который у нас идёт сразу после if, не создают новую область видимости. Только функции создают новые области видимости.
Для многих программистов, привыкших к Си, C++, C# или Java такое поведение очень неожиданное и неприятное. К счастью, благодаря гибкости функций JavaScript, можно обойти эту проблему. Чтобы создать временную область видимости внутри функции, достаточно сделать следующее:
function foo() { 
    var x = 1; 
    if (x) { 
        (function () { 
            var x = 2; 
            // какой-то код
        }()); 
    } 
    // x всё ещё 1. 
}

Такой подход достаточно гибок и может быть использован везде, где вам нужна временная область видимости, не только внутри блоков кода. Но я настаиваю на том, чтобы вы всё-таки потратили своё время, чтобы понять реализацию области видимости в JavaScript. Это довольно мощная особенность языка, которая мне очень нравится. Если вы понимаете область видимости, вам проще будет разобраться в «поднятии» переменных и объявлений функций.

Объявления, именование и «поднятие» переменных и функций


В JavaScript существует четыре основных способа появления идентификатора в области видимости:
  1. Внутренние механизмы языка: например, во всех областях видимости доступны this и arguments.
  2. Формальные параметры: у функций могут быть именованные формальные параметры, область видимости которых ограничена телом функции.
  3. Объявления функций: объявленные в виде function foo() {}.
  4. Объявления переменных: например, var foo;.

Интерпретатор JavaScript всегда незаметно для нас перемещает («поднимает») объявления функций и переменных в начало области видимости. Формальные параметры функций и встроенные переменные языка, очевидно, изначально уже находятся в начале. Это значит, что этот код:
function foo() { 
    bar(); 
    var x = 1; 
}

на самом деле интерпретируется так:
function foo() { 
    var x; 
    bar(); 
    x = 1; 
}

Оказывается, не важно, будет ли вообще выполнена строка, в которой происходит объявление. Следующие две функции эквивалентны:
function foo() { 
    if (false) { 
        var x = 1; 
    } 
    return; 
    var y = 1; 
} 
function foo() { 
    var x, y; 
    if (false) { 
        x = 1; 
    } 
    return; 
    y = 1; 
}

Обратите внимание, что присваивание значений переменным не поднимается вместе с их объявлением. Поднимаются только объявления переменных. В случае с функциями, поднимается вся функция целиком. Существуют два основных способа объявить функцию, давайте их рассмотрим:
function test() { 
    foo(); // TypeError "foo is not a function" 
    bar(); // "this will run!" 
    var foo = function () { // функциональное выражение, присвоенное локальной переменной 'foo' 
        alert("this won't run!"); 
    } 
    function bar() { // объявление функции с именем 'bar' 
        alert("this will run!"); 
    } 
} 
test();

в этом случае поднимается только функция bar. Идентификатор «foo» также поднимается, но не анонимная функция — она остаётся на месте.

Вот мы и описали основные моменты «поднятия» переменных и функций. Конечно, JavaScript не был бы сам собой, если бы не было особых случаев, в которых всё немного сложнее.

Разрешение имён


Самый важный особый случай, который стоит иметь в виду, — это порядок разрешения имён. Вспомните, есть четыре способа появления идентификаторов в области видимости. Именно в том порядке, в котором я их упомянул, и происходит разрешение имён. В общем случае, если имя уже определено, оно никогда не будет переопределено другой сущностью с таким же именем. То есть объявление функции имеет приоритет над объявлениями переменной с таким же именем. Но это совсем не означает, что присваивание переменной значение не заменит функцию, просто её определение будет проигнорировано.
Есть несколько исключений:
  • Встроенный идентификатор arguments ведёт себя странно. Он как будто объявляется сразу после формальных аргументов функции и перед объявлениями функций. Это означает, что если у функции есть формальный аргумент arguments, у него будет приоритет над встроенным, даже если его не передадут при вызове функции. Это плохая особенность JavaScript. Не используйте формальный аргумент с именем arguments.
  • Если вы попробуете использовать this в качестве идентификатора, произойдёт ошибка SyntaxError. Это хорошая особенность.
  • Если в списке формальных параметров функции несколько из них имеют одинаковое имя, тот параметр, который упоминается последним, имеет приоритет. Даже если его не передали при вызове функции.

Именованные функциональные выражения


Вы можете давать имена функциям, определённым с помощью функциональных выражений, используя синтаксис определения функций. Это не приводит к объявлению функции, а следовательно, имя функции ни добавляется в область видимости, ни поднимается вместе с телом функции в начало области видимости. Вот несколько строк, чтобы проиллюстрировать, что я имею в виду:
foo(); // TypeError "foo is not a function" 
bar(); // работает 
baz(); // TypeError "baz is not a function" 
spam(); // ReferenceError "spam is not defined" 
 
var foo = function () {}; // анонимное функциональное выражени (поднимается 'foo') 
function bar() {}; // объявление функции (поднимаются 'bar' и тело функции) 
var baz = function spam() {}; // именованное функциональное выражение (поднимается только 'baz') 
 
foo(); // работает 
bar(); // работает 
baz(); // работает 
spam(); // ReferenceError "spam is not defined" 

Как писать код, обладая такими знаниями


Итак, теперь вы понимаете область видимости и «поднятие» переменных и объявлений функций. Что это означает применительно к написанию кода на JavaScript? Самое главное — всегда объявлять ваши переменные, используя var. Я настаиваю на том, чтобы у вас был ровно один var на область видимости и чтобы он располагался в её начале. Если вы заставите себя так делать, у вас никогда не будет проблем, связанных с «поднятием». Тем не менее, это может привести к тому, что сложно следить за переменными, которые объявлены в текущей области видимости. Я рекомендую использовать JSLint с включённой опцией onevar, чтобы вынудить вас так делать. Если вы будете так всё делать, ваш код будет выглядеть примерно так:
/*jslint onevar: true [...] */ 
function foo(a, b, c) { 
    var x = 1, 
        bar, 
        baz = "something"; 
} 


Что говорит стандарт


Достаточно полезно обращаться к стандарту ECMAScript напрямую, чтобы понять, как всё работает. Вот что в нём говорится про объявление переменных и область видимости(секция 12.2.2):
Если инструкция переменной встречается внутри ОбъявленияФункции, переменные объявляются внутри локальной области видимости для данной функции согласно описанию в разделе 10.1.3. В противном случае они объявляются в глобальной области видимости (т.е. создаются как поля глобального объекта согласно описанию в разделе 10.1.3) с использованием атрибутов свойств { DontDelete }. Переменные создаются, когда происходит вход в область выполнения. Блок не определяет новой области выполнения. Только Программа и ОбъявлениеФункции создают новую область видимости. Переменные инициализируются при создании значением undefined. Переменной, для которой определён Инициализатор, присваивается значение его ВыраженияПрисваивания в момент выполнения ИнструкцииПеременной, а не в момент создания переменной.

Я надеюсь, эта статья пролила немного света на особенность JavaScript, которая так часто многих приводит в замешательство. Я старался быть как можно более последовательным, чтобы не запутать вас ещё сильнее. Если я в чём-то ошибся или что-то пропустил, пожалуйста, дайте мне знать.
Tags:
Hubs:
+109
Comments 71
Comments Comments 71

Articles