JavaScript: Что такое hoisting
Что такое hoisting?
Посмотрите на приведённый ниже код и угадайте, что происходит при его запуске:
console.log(foo);
var foo = 'foo';
Вас может удивить, что этот код выводит undefined
и не даёт сбоев или ошибок — даже если foo
назначается того как мы выводим его в console.log()
!
Это связанно с тем, что интерпретатор JavaScript разделяет объявление и назначение функций и переменных: он "поднимает" ваши объявления в верхнюю часть содержащей их области видимости перед выполнением.
Этот процесс называется hoisting/хоистинг
— подъём, и он позволяет нам использовать foo
перед его объявлением в нашем примере выше.
Давайте подробно рассмотрим hoisting функций и переменных, что бы понять что это означает и как работает.
Hoisting переменных в 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';
Hoisting переменной действует по-разному, в зависимости от того, как объявлена переменная. Начнём с понимания поведения переменных var
.
Hoisting переменных объявленных с помощью var
Когда интерпретатор производит hoisting переменной, объявленную с помощью var
, он инициализирует её значением undefined
. Первая строка кода, приведённого ниже, выведет undefined
:
console.log(foo); // undefined
var foo = 'bar';
console.log(foo); // "bar"
Как мы определили ранее, hoisting происходит из разделения интерпретатором объявления переменной и присваивания. Мы можем добиться того же поведения вручную, разделив объявление и присваивание на два шага:
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 даёт нам доступ к переменным до того, как они объявлены». Такое поведение необычная часть JavaScrip и может привести к ошибкам. Использование переменной перед её объявлением, обычно, не желательно.
К счастью, переменные let
и const
, представленные в ECMAScript 2015, ведут себя по-другому.
Hoisting переменных объявленных с помощью 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
.
Вот пример с явным блоком, который показывает начало и конец TDZ переменной 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
объявлен, но не инициализирован — мы должны знать, что используем его перед инициализацией (источник Axel Rauschmayer).
Однако, это не тот случай, когда используется переменная var
перед объявлением, потому что она инициализируется со значением undefined
перед подъёмом:
console.log(typeof foo); // "undefined"
var foo = 'foo';
Более того, это удивительно, потому что мы можем без ошибок проверить тип несуществующей переменной. typeof
безопасно возвращает строку:
console.log(typeof foo); // "undefined"
Фактически, введение let
и const
нарушило гарантию typeof
всегда возвращать строковое значение для любого операнда.
Hoisting функций в JavaScript
Объявления функций тоже поднимаются. Hoisting функции позволяет вызвать функцию до того, как она будет определена. Например, следующий код выполнится успешно и выведет "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: foo is not defined
Как использовать hoisting в Javascript
Hoisting переменных
Из-за путаницы, которую может создать hoisting переменных, лучше избегать использование переменных до их объявления. Если вы пишете код в новом проекте, вы должны использовать let
и const
для обеспечения этого.
Если вы работаете со старой кодовой базой или вынуждены использовать var
по другой причине, MDN рекомендует писать объявления var
как можно ближе к верхнему краю их области видимости. Это сделает область видимости ваших переменных более явной.
Вы также можете рассмотреть возможность использования правила ESLint
no-use-before-define
, которое гарантирует, что вы не используете переменную до её объявления.
{
"no-use-before-define": ["error", { "functions": true, "classes": true, "variables": true }]
}
Hoisting функций
Hoisting функций полезен тем, что мы можем скрыть реализацию функции дальше в файле и позволить читателю сфокусироваться на том, что делает код. Другими словами, мы можем открыть файл и посмотреть, что делает код, без предварительного понимания, как он реализован.
Возьмём следующий выдуманный пример:
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)
Заключение
Спасибо за чтение, и я надеюсь, что эта статья помогла вам узнать о hoisting в JavaScript.