JavaScript: Что такое hoisting

Источник: «What is Hoisting in JavaScript?»
В JavaScript hoisting позволяет использовать функции и переменные до их объявления. В этой статье мы узнаем что такое 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.

Дополнительные материалы

Предыдущая Статья

CSS: Современные селекторы псевдоклассов

Следующая Статья

Защитный CSS — коллекция сниппетов