Разбираемся с “поднятием” (hoisting) в JavaScript

Stas Bagretsov
Feb 23, 2018 · 8 min read

Это перевод статьи Understanding Hoisting in JavaScript

В этом руководстве вы изучите то, как срабатывает всеми извезтный механизм “поднятия” в JavaScript. Ну или в оригинальном названии hoisting. Однако, перед тем как углубиться в детали, давайте узнаем что это вообще такое и как оно работает на самом деле.

Поднятие или hoisting — это механизм в JavaScript, в котором переменные и объявления функций, передвигаются вверх своей области видимости перед тем, как код будет выполнен.

Как следствие, это означает то, что совершенно неважно где были объявлены функция или переменные, все они передвигаются вверх своей области видимости, вне зависимости от того локальная она или же глобальная.

Стоит отметить то, что механизм “поднятия” передвигает только объявления функции или переменной. Назначения переменным остаются на своих местах.

В общем, если вы когда-либо удивлялись, почему вы могли вызывать функции перед тем, как они написаны в коде, то читайте дальше.

Undefined vs ReferenceError

Перед тем, как мы начнем серьёзно углубляться в этот вопрос, давайте проясним несколько вещей.

 console.log(typeof variable); // Выводит: undefined

Это приведет нас к первому заключению. В JavaScript, необъявленной переменной при выполнении кода назначается значение undefined , а так же и тип undefined.

Вторым заключением будет:

console.log(variable); // Выводит: ReferenceError: variable is not defined

В JavaScript, ReferenceError появляется при попытке доступа к предварительно необъявленной переменной.

Поведение JavaScript при работе с переменными становится довольно утонченным делом из-за «поднятия». Мы увидим это более детально в следующих параграфах.

«Поднятие» переменных

Ниже вы видите цикл работы JavaScript, показывающий последовательность, в которой происходит объявление и инициализация переменных.

Однако, в JavaScript мы можем объявлять и инициализировать наши переменные одновременно, как в этом ну просто самом распространенном примере:

var a = 100;

Запомните и держите в уме одну важную деталь, JavaScript непреклонно сначала объявляет, а уже затем инициализирует наши переменные.

Как упоминалось ранее, все переменные и объявления функций поднимаются вверх своей области видимости. Мне также стоит добавить, что объявление переменных происходит перед выполнением кода.

Но однако, необъявленные переменные не существуют до тех пор, пока код назначающий их не будет выполнен. Следовательно, указание значения для необъявленной переменной, тут же создаёт её как глобальную переменную, когда назначение будет выполнено. Это говорит о том, что все необъявленные переменные это по факту глобальные переменные.

Чтобы продемонстрировать это поведение, давайте посмотрим на следующий код.

function hoist() {
a = 20;
var b = 100;
}

hoist();

console.log(a);
/*Доступ как к глобальной переменной вне функции hoist()Выводит: 20*/console.log(b);/*Как только b была назначена, она заключена в рамки области видимости функции hoist(). Что означает то, что мы не можем вывести её за рамки функции.Вывод: ReferenceError: b is not defined*/

Так как это одна из причуд работы JavaScript с переменными, рекомендуется всегда объявлять их, вне зависимости от их положения в коде, в функции они или в глобальной области видимости.

Это ясно указывает на то, как движок JavaScript должен с ними работать во время выполнения кода.

ES5

var

Областью видимости переменной, объявленной через var, является её настоящий контекст выполнения. Это подходит как и для замыкания функции, так и для переменных объявленных вне любой функции, то есть глобальных. Давайте посмотрим на несколько примеров, чтобы понять, что это означает.

Глобальные переменные

console.log(hoist); // Вывод: undefined

var hoist = 'The variable has been hoisted.';

Мы ожидали, что результат в логе будет: ReferenceError: hoist is not defined, но вместо этого нам вывело undefined.

Почему так произошло?

JavaScript «поднял» объявление переменной. Вот как это выглядит для движка JavaScript:

var hoist;

console.log(hoist); // Вывод: undefined
hoist = 'The variable has been hoisted.';

Поэтому мы можем использовать переменные перед тем как мы объявим их. Тем не менее, в этом вопросе нужно быть аккуратными, так как «поднятая» переменная инициализируется со значением undefined. Лучшим вариантом, как говорилось выше, было бы объявить и инициализировать нашу переменную перед её непосредственным использованием.

Переменные в области видимости функции

Как мы видели до этого, переменные в глобальной области видимости поднимаются вверх. Дальше давайте посмотрим на то, как ведут себя переменные при «поднятии» в функциях .

function hoist() {
console.log(message);
var message='Hoisting is all the rage!'
}

hoist();

Подумайте, что нам выдаст эта функция? Если вы предположили — undefined, то вы оказались правы. Если нет, то не переживайте, мы скоро доберемся до сути и объясним почему так.

Вот как движок видит код выше:

function hoist() {
var message;
console.log(message);
message='Hoisting is all the rage!'
}

hoist(); // Вывод: undefined

Объявление переменной var message, область видимости которой заканчивается в функции hoist(), «поднимается» наверх функции.

Чтобы избегать таких ловушек, нам всегда нужно убеждаться в том, что мы объявляем и инициализируем переменные перед их использованием.

function hoist() {
var message='Hoisting is all the rage!'
return (message);
}

hoist(); // Вывод: Hoisting is all the rage!

Strict Mode или «Строгий режим»

Спасибо такой полезной штуковине как, strict-mode в es5 версии JavaScript, с помощью которой вы можете быть внимательнее при объявлении переменных. Включая strict mode, мы включаем некий ограниченный вариант JavaScript, который не будет возиться с использованием переменных перед их объявлением.

Запуск кода в strict mode:

Устраняет некоторые скрытые ошибки в JavaScript, изменяя их на явную выдачу ошибок, которые будут в последствии выданы движком.

Устраняет ошибки, которые затрудняют движкам JavaScript выполнять оптимизацию.

Запрещает некоторый синтаксис, который с большой долей вероятности будет уже идти из коробки в будущих версиях JavaScript.

Мы включаем «строгий режим», заранее указывая в нашем файле или функции следующее:

'use strict';

// или
"use strict";

Давайте протестируем.

'use strict';

console.log(hoist); // Вывод: ReferenceError: hoist is not defined
hoist = 'Hoisted';

Мы видим, что вместо того, чтобы указать, то что мы пропустили объявление нашей переменной, use strict остановил нас на полпути, выдав Reference error. Попробуйте это без использования use strict и понаблюдайте что произойдет.

Тем не менее, use-strict ведет себя по разному в разных браузерах, так что будет вполне благоразумно провести тестирование функционала перед использованием в работе.

ES6

Тут нам представляет интерес то, как этот стандарт влияет на объявление и инициализацию JavaScript переменных.

let

Перед тем как идти дальше, стоит отметить то, что переменные объявленные через let заключены в область видимости блока, а не функции. Это очень важно, но это не должно нас сейчас волновать.

Вкратце, это просто говорит о том, что область видимости переменной привязана к блоку, в котором она объявлена, а не к функции в которой она объявлена.

Давайте посмотрим на поведение let.

console.log(hoist); // Вывод: ReferenceError: hoist is not defined ...
let hoist = 'The variable has been hoisted.';

Как и с var, мы ожидаем, что выведется в лог undefined. Но так как es6 let недружелюбно относится к необъявленным переменным, интерпретатор выдает нам Reference ошибку.

Это еще раз доказывает то, что надо сначала объявлять наши переменные.

Однако, всё еще нужно быть аккуратными. Пример ниже выдаст undefined вместо Reference error.

let hoist;

console.log(hoist); // Вывод: undefined
hoist = 'Hoisted'

Следовательно, чтобы не напороться на предыдущие ошибки, нам нужно сначала объявить переменную, а потом назначить ей значение и только потом уже её использовать.

const

const была представлена в es6 для того, чтобы можно было сделать неизменные переменные. Да, именно это, переменные значение которых не может быть изменено или переназначено.

С const, как и с let, переменные поднимаются вверх блока кода.

Давайте посмотрим, что происходит если мы попытаемся переназначить значение, прикрепленное к const переменной.

const PI = 3.142;

PI = 22/7; // Давайте изменим значение PI
console.log(PI); // Вывод: TypeError: Assignment to constant variable.

Как const изменяет объявление переменной? Давайте посмотрим.

console.log(hoist); // Output: ReferenceError: hoist is not defined
const hoist = 'The variable has been hoisted.';

Как и с let, вместо тихого спокойного выхода с undefined, движок спасает нас, выдавая Reference error.

Тоже самое происходит при использовании const в функциях.

function getCircumference(radius) {
console.log(circumference)
circumference = PI*radius*2;
const PI = 22/7;
}

getCircumference(2) // ReferenceError: circumference is not defined

С const, es6 идет ещё дальше. Движок выдает ошибку, если мы используем константу перед её объявлением или инициализацией.

Наш линтер быстренько информирует нас об этом просчете:

PI was used before it was declared, which is illegal for const variables.

Глобально,

const PI;console.log(PI); // Ouput: SyntaxError: Missing initializer in const declarationPI=3.142;

Следовательно, константные переменные должны быть объявлены и инициализированы перед использованием.

Как пролог к этой секции, важно заметить, что JavaScript «поднимает» переменные let и const. Разница лишь в том, как он инициализирует их. Переменные объявленные с let и const остаются неинициализированными в начале выполнения, в то время как переменные объявленные с var инициализируются со значением undefined.

Поднятие функций

JavaScript функции могут классифицироваться как объявленные функции, так и как функциональные выражения. Далее мы узнаем как «поднятие» влияет на оба типа.

Объявленные функции

Такие функции полностью поднимаются вверх кода. Теперь понятно почему JavaScript позволяет нам вызывать функции прежде, чем мы их объявим по упоминанию в коде.

hoisted(); // Вывод: "This function has been hoisted."

function hoisted() {
console.log('This function has been hoisted.');
};

Функциональные выражения, однако, не поднимаются.

expression(); //Вывод: "TypeError: expression is not a function

var expression = function() {
console.log('Will this work?');
};

Как мы можем видеть выше, объявление переменной var expression поднимается, но его назначение как функции — нет. Следовательно, движок выдаст TypeError как увидит expression в виде переменой, а не функции.

Порядок по приоритетам

Очень важно помнить несколько вещей, объявляя JavaScript функции и переменные.

Назначение переменных имеет приоритет перед объявлением функции.

Объявление функции имеет приоритет перед объявлением переменной.

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

Давайте посмотрим как это работает.

Назначение переменной над объявлением функции.

var double = 22;

function double(num) {
return (num*2);
}

console.log(typeof double); // Вывод: number

Объявление функции над объявлением переменной.

var double;

function double(num) {
return (num*2);
}

console.log(typeof double); // Вывод: function

Даже если мы изменим позиции объявлений, JavaScript движок все равно бы взял double функцию.

«Поднятие» классов

Классы в JavaScript могут также классифицироваться как объявления и выражения.

Объявления классов

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

var Frodo = new Hobbit();
Frodo.height = 100;
Frodo.weight = 300;
console.log(Frodo); // Вывод: ReferenceError: Hobbit is not defined

class Hobbit {
constructor(height, weight) {
this.height = height;
this.weight = weight;
}
}

Уверен, что вы заметили то, что вместо undefined мы получили Reference error. Это подвергает сомнению нашу позицию, что объявления классов «поднимаются».

Если мы взглянем на линтер, то там мы увидим полезный совет.

Hobbit was used before it is declared, which is illegal for class variables

Поэтому, чтобы получить доступ к классу, вам нужно сначала его объявить.

class Hobbit {
constructor(height, weight) {
this.height = height;
this.weight = weight;
}
}

var Frodo = new Hobbit();
Frodo.height = 100;
Frodo.weight = 300;
console.log(Frodo); // Вывод: { height: 100, weight: 300 }

Выражения классов

Так же как и свои коллеги по функциям, выражения классов не «поднимаются».

Вот пример анонимного варианта выражения класса.

var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square); // Вывод: TypeError: Polygon is not a constructor

var Polygon = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

А вот пример с названным выражением класса.

var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square); // Output: TypeError: Polygon is not a constructor


var Polygon = class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

А вот правильный порядок:

var Polygon = class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square);

Предостережение

Есть много споров о JavaScript es6 let, const переменных и классах, о том как они «поднимаются», приблизительно «поднимаются» или не «поднимаются» вообще. Некоторые утверждают, что они на самом деле «поднимаются», но не инициализируются, в то время как некоторые утверждают то, что они не «поднимаются» вообще.

Заключение

Давайте подведем итоги того, что мы изучили:

1. Используя es5 переменную var, попытки использования необъявленных переменных приведут к тому, что переменной будет назначено значение undefined при «поднятии».

2. Используя переменные es6 let и const, использование переменных приведет к Reference Error, потому что переменная останется неинициализированной при выполнении.

Тем не менее,

1. Нам нужно взять в привычку, объявлять и инициализировать JavaScript переменные перед использованием.

2. Использование strict mode в JavaScript es5 может помочь выявить необъявленные переменные.

Stas Bagretsov

Written by

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books