Поднятие в JavaScript

Photo by PhotoMIX Company from Pexels

Поднятие (англ. 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]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх