JavaScript: Демистифицируем Hoisting в JS


Введение

Одной из главных причин написания этой статьи стало то, что однажды я услышал, как несколько моих коллег говорили о hoisting в JS, то есть о поднятии состояния. В то время мне было очень трудно понять их точку зрения, поэтому я начал искать соответствующие статьи в Интернете. Оказалось, что это не то, в чем можно разобраться в один момент, поэтому я начал этот блог, надеясь раскрыть еще один пункт знаний о JS.

Что такое Hoisting на самом деле?

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

console.log(a);
// ReferenceError: a is not defined
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы обнаружили, что браузер выдаст ошибку reference, сообщив, что a еще не определена. А что если?

console.log(a); // undefined
var a;
Войти в полноэкранный режим Выйти из полноэкранного режима

Как это может произойти? Логически рассуждая, код выполняется строка за строкой, и ошибка должна быть ReferenceError? Почему она undefined?

Это происходит из-за поднятия JS, поднятия состояния. Подъем отражается в том, что строка var a «поднимается» на самый верх. Поэтому, когда JS движок (V8) разбирает этот код, на самом деле это работает следующим образом:

var a;
console.log(a); // undefined
Вход в полноэкранный режим Выход из полноэкранного режима

Но обратите внимание! Это всего лишь воображение, а не то, что JS-движок физически переместил код!

Что если я изменю код так, чтобы он выглядел следующим образом?

console.log(a); // undefined
var a = 10;
Войти в полноэкранный режим Выйти из полноэкранного режима

По-прежнему undefined, но почему? Разве var a = 10 не должен быть поднят на самый верх, таким образом, вычисление a должно быть равно 10?

Здесь вводится другая концепция: только объявление переменных будет поднято, а присвоение - нет.

Так что на самом деле код выше выглядит в глазах JS-движков вот так:

var a;
console.log(a); // undefined
a = 10;
Вход в полноэкранный режим Выход из полноэкранного режима

Так что на самом деле var a = 10 можно разложить на два шага. Сначала будет поднят var = a, что является частью declaration, а на втором этапе a = 10 останется и не будет участвовать в процессе подъема.

Пока все просто, но потом может начаться хаос. Взгляните на следующий пример:

function func(h) {
  console.log(h);
  var h = 3;
}

func(10);
Вход в полноэкранный режим Выход из полноэкранного режима

Сначала я подумал, что это смешно, что тут можно сказать, не так ли?

function func(h) {
  var h;
  console.log(h);
  h = 3;
}

func(10);
Войти в полноэкранный режим Выйти из полноэкранного режима

Так что, конечно же, на выходе будет undefined! Что ж, реальный результат сразу же бьет по лицу, и на выходе получается 10.

На самом деле, процесс преобразования и подъема правильный, но единственное — мы забыли вызвать функцию. Так что на самом деле все выглядит следующим образом:

function func(h) {
  var h = 10;
  var h;
  console.log(h);
  h = 3;
}

func(10);
Войти в полноэкранный режим Выход из полноэкранного режима

Но это все равно немного странно, потому что даже в этом случае var h вызывается снова, прежде чем принять значение h без присваивания. Значит, оно все еще должно быть undefined?

Что ж, давайте попробуем более простой пример:

var a = 10;
var a;
console.log(a); // 10
Вход в полноэкранный режим Выход из полноэкранного режима

Используя приведенный выше метод декомпозиции, на самом деле это можно представить следующим образом:

var a;
var a;
a = 10;
console.log(a);
Вход в полноэкранный режим Выход из полноэкранного режима

Это выводит 10, что вполне приемлемо. В то время я думал: «Что, черт возьми, происходит! Откуда только взялось столько неразумных правил? Кто может в этом разобраться? Просто сначала потерпите и посмотрите на последний пример:

console.log(a);
var a;
function a() {}
Вход в полноэкранный режим Выход из полноэкранного режима

Согласно приведенному выше методу декомпозиции, мы хотим разложить это на следующее:

var a;
console.log(a);
function a() {}
Вход в полноэкранный режим Выйти из полноэкранного режима

На выходе должно получиться undefined, верно? Результат — еще одна большая пощечина, выводящая [Function: a]. В этот момент я уже бил себя по рукам. Оказывается, в дополнение к концепции подъема состояния в назначениях объявления переменных, применяется и объявление функций. Более того, приоритет соответствия поднятия состояния при объявлении функции выше, чем при объявлении переменной. Поэтому, на самом деле, приведенный выше код следует представить следующим образом:

// Prioritized
function a() {}
var a;
console.log(a);
Вход в полноэкранный режим Выход из полноэкранного режима

Сказав так много, давайте немного разберемся:

1. Both variable declarations and function declarations will participate hoisting processes

2. Variables are promoted only by declaration, not by assignment

3. Don't forget that there are also passed parameters in the function
Вход в полноэкранный режим Выход из полноэкранного режима

let const & hoisting

Вы заметили, что когда выше была представлена концепция hoisting, для объявления переменных использовалось ключевое слово var. Однако все знают, что в ES6 появились let и const, и мейнстрим больше не рекомендует использовать var, а вместо этого использует let с const.

Для let и const, по сути, концепция подъема схожа. Давайте рассмотрим несколько примеров.

console.log(a);
let a;
// ReferenceError: a is not defined
Вход в полноэкранный режим Выход из полноэкранного режима

Магия! Вывод: ReferenceError: a is not defined. Что ж, это действительно более здравый смысл, верно? Но означает ли это, что не существует такого понятия, как подъем при использовании let для объявления функций? Если так, то это было бы замечательно, но, к сожалению, это не так. Взгляните на приведенный ниже пример:

var a = 10;
function func() {
  console.log(a);
  let a;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если let не имеет подъема состояния, то на выходе должно быть 10, верно? Потому что снаружи находится var a = 10, и тогда let a не имеет подъема состояния. Опять неверно, на выходе ReferenceError: a is not defined. Так что на самом деле let тоже поднимается, но поведение процесса продвижения let может отличаться от поведения var, поэтому на первый взгляд кажется, что подъема нет. Что касается того, как это работает, мы обсудим это позже.

Здесь сделайте паузу. Если вы просто хотите немного узнать о том, что такое hoisting, вы можете остановиться на этом, потому что на самом деле, если вы будете правильно использовать и применять let const, а затем правильно объявлять и присваивать свои переменные, то все будет в порядке. Но если вы хотите понять все более глубоко и основательно, просто оставайтесь со мной и продолжайте читать. Далее мы обсудим два важных вопроса о hoisting.

  1. Почему именно подъем?
  2. Как именно работает подъемник?

Зачем нужен подъемник?

Рассмотрев некоторые правила и концепции подъемника, упомянутые выше, мы можем почувствовать некоторые преимущества, которые он дает. Чтобы ответить на этот вопрос, можно подумать с противоположной стороны: «Что было бы, если бы не было подъемника?».

  1. Без hoisting нам пришлось бы объявлять переменную перед ее использованием. Но на самом деле это очень хорошо. В конце концов, все так пишут, когда программируют. Я полагаю, что никто не будет кодить и при этом думать о механизме подъема в JS, не объявлять переменные и использовать их напрямую, верно? Так что это на самом деле хорошо.

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

  3. Последний пункт более интересен. Без подъема мы не смогли бы вызывать друг друга между функциями. Что это значит? Взгляните на приведенный ниже код:

function loop_1() {
  console.log("loop 1");
  loop_2();
}

function loop_2() {
  console.log("loop 2");
  loop_1();
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот код несложно понять. loop_1 и loop_2 вызывают друг друга. Но есть проблема, если нет подъема, как loop_1 может быть выше loop_2, и в то же время loop_2 также выше loop_1. Этот код не работал бы без подъема.

Как вывод, подъем — это решение этих проблем!

Как именно работает подъем?

Прежде всего, нам необходимо ввести понятие Execution Context JavaScript, которое ниже будет сокращенно называться EC. Концепция EC заключается в том, что каждый раз, когда вводится функция, она будет иметь EC, а затем EC будет заталкиваться в стек. Когда функция будет выполнена, EC будет вытолкнут.

В общем, EC хранит информацию соответствующих функций. Когда функции что-то нужно, она обращается к своему собственному EC, чтобы найти это.

Каждый EC имеет соответствующий VO (Variable Object). В этом VO хранится вся информация, включая переменные в функции, функцию и параметры в функции. Механизм поиска в VO означает следующее:

Если взять в качестве примера var a = 10 выше, то первым шагом будет добавление нового атрибута a в VO, а затем найти атрибут с именем a и установить его в 10.

Step1: var a
Step2: a = 10
Войдите в полноэкранный режим Выход из полноэкранного режима

В функции так много всего, как же поместить все в VO каждого EC?

Для параметров, они будут непосредственно помещены в VO, если некоторые параметры не переданы со значением, то их значение будет инициализировано и станет undefined. Рассмотрим следующий пример:

function func(a, b, c) {
    ...
    ...
}

func(10)
Войти в полноэкранный режим Выйти из полноэкранного режима

Если вызвать вышеуказанную функцию, то VO будет выглядеть следующим образом:

// VO
{
    a: 10,
    b: undefined,
    c: undefined
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если в функции есть еще одно объявление функции, оно также добавляется в VO, без проблем. Но что если имя функции случайно совпадает с именем переменной?

function func(a) {
    function a() {
        ...
        ...
    }
}

func(10)
Вход в полноэкранный режим Выйти из полноэкранного режима

VO выглядит следующим образом:

{
    a: function a
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Для объявления переменной внутри функции, она будет помещена в VO в конце. Если в VO уже есть атрибут с тем же именем, переменная будет игнорироваться напрямую, а исходное значение не будет изменено.

Вкратце, мы можем думать о действиях VO, упомянутых выше, как о предварительной работе перед выполнением функции. Порядок действий следующий:

Step1: Put the parameters into VO, and then see if there are any incoming values respectively. The parameters are matched in the order in which they are declared. If they are not matched, they will be assigned the value undefined.

Step2: Find the member methods in the function, in other words, other functions, and put it into the VO. If it has the same name as any property in the current VO, overwrite the old one.

Step3: Finally, find the variable declaration in the function and put it in VO. If it has the same name as any property in the current VO, the current state will prevail.
Войти в полноэкранный режим Выход из полноэкранного режима

Сказав так много, давайте вернемся к примеру, который мы приводили выше:

function func(h) {
  console.log(h);
  var h = 3;
}

func(10);
Вход в полноэкранный режим Выход из полноэкранного режима

Таким образом, выполнение каждой функции можно разделить на два этапа. Во-первых, она войдет в контекст выполнения функции, а затем начнет подготовку своего собственного VO. В приведенном выше примере, во-первых, поскольку в вызове присутствуют параметры, сначала в VO будет объявлена переменная h, значение которой будет равно 10. Затем, поскольку функция-член не найдена в функции, она остается неизменной. Наконец, найдите var h = 3, это оператор объявления переменной, поэтому он должен быть добавлен в VO, но поскольку в VO в это время уже есть переменная с именем h, поэтому VO не изменяется. Таким образом, VO этой функции было установлено.

// func() VO
{
    h: 10
}
Вход в полноэкранный режим Выход из полноэкранного режима

После создания VO начните выполнение этой функции. Когда код выполнится до console.log(h), он просмотрит VO и обнаружит, что существует переменная h со значением 10, так что 10 — это она и есть. Таким образом, на вопрос выше получен ответ, действительно выводится 10 Да!

А что если код изменить на такой?

function func(h) {
  console.log(h);
  var h = 3;
  console.log(h);
}

func(10);
Вход в полноэкранный режим Выйти из полноэкранного режима

Первый вывод, конечно же, будет 10, а второй — 3.

На самом деле, процесс создания VO такой же, как и выше, поэтому при выполнении первый вывод будет 10, без проблем. А поскольку при выполнении в VO меняется h в строке 3, второй вывод, конечно же, будет 3!

Оцените статью
devanswers.ru
Добавить комментарий