Поднятие (англ. hoisting) в JavaScript позволяет использовать функции и переменные до их объявления. В этой статье мы разберем, что собой представляет поднятие и как оно работает.
Что такое поднятие в JavaScript?
Взгляните на этот код и попробуйте угадать, что произойдет при его запуске:
console.log(foo); var foo = 'foo';
Возможно, вы удивитесь, но в выводе вы получите не ошибку, а undefined
. Даже несмотря на то, что значение foo назначено позже того, как мы использовали его в console.log
!
Дело в том, что интерпретатор JavaScript разделяет объявление и назначение значений для функций и переменных. Ваши объявления «поднимаются» вверх их области видимости перед выполнением.
Этот процесс называется поднятием. Именно благодаря ему мы можем использовать переменную foo в нашем примере еще до того, как она объявлена.
Давайте еще немного глубже погрузимся в тему поднятия функций и переменных и постараемся разобраться, как это все работает.
Поднятие переменных в JavaScript
Чисто в качестве напоминания: мы объявляем переменные при помощи операторов var, let и const. Например:
var foo; let bar;
Мы назначаем значение переменной, используя оператор присваивания:
// Объявление var foo; let bar; // Присваивание значения foo = 'foo'; bar = 'bar';
Во многих случаях объявление и присваивание значения комбинируются в один шаг:
var foo = 'foo'; let bar = 'bar'; const baz = 'baz';
Поднятие переменных работает по-разному, в зависимости от того, как была объявлена переменная. Давайте для начала разберемся в поведении переменных, объявленных при помощи var
.
Поднятие переменных, объявленных при помощи var
Когда интерпретатор поднимает переменную, объявленную при помощи var, он инициализирует ее со значением undefined
. Первая строка следующего кода в выводе даст undefined
:
console.log(foo); // undefined var foo = 'bar'; console.log(foo); // "bar"
Как мы установили ранее, в основе поднятия лежит тот факт, что интерпретатор разделяет объявление переменных и присвоение им значений. Мы можем сделать то же самое вручную, разделив объявление и присвоение значения на два шага:
var foo; console.log(foo); // undefined foo = 'foo'; console.log(foo); // "foo"
Первый вывод console.log(foo)
— undefined
, потому что foo
поднимается и получает значение по умолчанию (а не потому что переменная вообще не объявлена). А вот использование необъявленной переменной приведет к ошибке ReferenceError:
console.log(foo); // Uncaught ReferenceError: foo is not defined
Использовав необъявленную переменную до присвоения ей значения, вы также получите ошибку ReferenceError, потому что здесь нет объявления, которое могло бы подняться:
console.log(foo); // Uncaught ReferenceError: foo is not defined foo = 'foo'; // Присвоение значения необъявленной переменной допустимо
Теперь вы, вероятно, думаете: «Хм. Это как-то странно, что JavaScript позволяет получить доступ к переменным до того как они объявлены». Это поведение — необычная часть JavaScript. Оно может привести к ошибкам, поэтому использование переменной до ее объявления обычно нежелательно.
В ECMAScript 2015 были представлены операторы let
и const
. Переменные, объявленные с их помощью, ведут себя иначе.
Поднятие переменных, объявленных при помощи let и const
Переменные, объявленные при помощи let
и const
, поднимаются, но не инициализируются с дефолтным значением. Попытка доступа к let
или const
-переменной до ее объявления приведет к ReferenceError:
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 'bar'; // Переменные, объявленные с помощью const, ведут себя так же
Обратите внимание, что интерпретатор по-прежнему поднимает foo: сообщение об ошибке говорит нам, что где-то эта переменная инициализирована.
Временная мертвая зона
Попытавшись обратиться к let
или const
-переменной до ее объявления, мы получаем ошибку, и происходит это из-за временной мертвой зоны (temporal dead zone, TDZ).
TDZ начинается в начале области видимости переменной и заканчивается ее объявлением. Обращение к переменной в TDZ приводит к выбросу ReferenceError.
Вот пример понятного блока, показывающего начало и конец временной мертвой зоны foo:
{ // Начало TDZ для foo let bar = 'bar'; console.log(bar); // "bar" console.log(foo); // ReferenceError, потому что мы в TDZ let foo = 'foo'; // Конец TDZ для foo }
TDZ также присутствует в дефолтных параметрах функции, которые оцениваются слева направо. В следующем примере bar находится в TDZ, пока не получает значение по умолчанию:
function foobar(foo = bar, bar = 'bar') { console.log(foo); } foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
Но этот код работает, потому что мы можем обратиться к foo вне ее TDZ:
function foobar(foo = 'foo', bar = foo) { console.log(bar); } foobar(); // "foo"
typeof во временной мертвой зоне
Если вы используете let
или const
-переменную как операнд оператора typeof
в TDZ, вы получите ошибку:
console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 'foo';
Это поведение согласуется с другими примерами с let
и const
в TDZ, которые мы уже видели. Здесь мы получаем ReferenceError, потому что foo
объявлена, но не инициализирована. Мы должны знать, что используем ее до инициализации.
Но это не касается использования var
-переменной до ее объявления, потому что такая переменная при поднятии инициализируется со значением undefined
:
console.log(typeof foo); // "undefined" var foo = 'foo';
Кроме того (и это удивительно), мы можем проверить тип несуществующей переменной и не получить ошибку. typeof
безопасно возвращает строку:
console.log(typeof foo); // "undefined"
Фактически, появление let
и const
сломало гарантию того, что typeof
всегда возвращает строковое значение для любого операнда.
Поднятие функций в JavaScript
Объявления функций тоже поднимаются. Это позволяет нам вызывать функцию до ее объявления. Например, следующий код успешно запускается и выдает результат «foo»:
foo(); // "foo" function foo() { console.log('foo'); }
Обратите внимание, что поднимаются только объявления функций, не функциональные выражения. В этом есть смысл: как мы только что разобрали, присвоения значений переменным не поднимаются.
Если мы попытаемся вызвать переменную, которой было присвоено в качестве значения функциональное выражение, мы получим TypeError или ReferenceError — в зависимости от области видимости переменной:
foo(); // Uncaught TypeError: foo is not a function var foo = function () { } bar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization let bar = function () { } baz(); // Uncaught ReferenceError: Cannot access 'baz' before initialization const baz = function () { }
Это отличается от вызова вообще не объявленной функции, который вызвал бы другую ошибку — ReferenceError:
foo(); // Uncaught ReferenceError: baz is not defined
Как использовать поднятие в JavaScript
Поднятие переменных
Из-за того, что поднятие var
может привести к путанице, лучше не использовать переменные до их объявления. Если вы пишете проект с нуля, используйте let
и const
, чтобы следование этому подходу стало неизбежным.
Если вы работаете над старой кодовой базой или вам еще по какой-то причине приходится использовать var, MDN рекомендует писать var-объявления как можно выше в области видимости. Таким образом скоуп ваших переменных будет яснее.
Вы также можете подумать над использованием правила no-use-before-define в ESLint, чтобы точно не использовать переменные до их объявления.
Поднятие функций
Поднятие функций — вещь полезная. Мы можем спрятать реализацию функции внизу файла и таким образом дать читателю сосредоточиться на том, что вообще делает наш код. То есть, благодаря такому подходу, мы можем открыть файл и сразу увидеть, что делает код, не разбираясь в его реализации.
Вот вам надуманный пример:
resetScore(); drawGameBoard(); populateGameBoard(); startGame(); function resetScore() { console.log("Resetting score"); } function drawGameBoard() { console.log("Drawing board"); } function populateGameBoard() { console.log("Populating board"); } function startGame() { console.log("Starting game"); }
Взглянув на этот код, мы сразу получаем представление о том, что он делает, и нам для этого не приходится сначала прочесть все объявления функций.
Тем не менее, использование функций до их объявления — дело вкуса. Некоторые разработчики, например Wes Bos, предпочитают другой путь. Они помещают функции в модули, которые можно импортировать при необходимости. (Источник: Wes Bos).
Руководство по стилю от Airbnb идет еще дальше. Чтобы предотвратить обращение до объявления, гайд советует отдавать предпочтение именованным функциональным выражениям, а не объявлениям:
«Объявления функций поднимаются, а это означает, что слишком велик соблазн обратиться к функции до того, как она определена в файле. Это вредит читаемости и поддерживаемости.
Если вы полагаете, что определение функции настолько большое и сложное, что помешает читателю понять остальной файл, возможно, пора выделить это определение в модуль!»
(Источник — Airbnb JavaScript Style Guide).
Заключение
Спасибо за внимание! Надеюсь, эта статья помогла вам познакомиться с темой поднятия в JavaScript.
Перевод статьи «What is Hoisting in JavaScript?».
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]