Эта статья станет частью серии, в которой я хочу объяснить замыкание в javascript. Замыкание — одна из самых важных и ценных возможностей javascript. Это тема, которая вызвала много вопросов на интервью, и иногда она создает кошмар для многих разработчиков Javascript.

Хм, вы можете быть удивлены, почему?

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

Прежде чем изучать математику в средней школе, вы должны иметь базовые знания по математике в начальной школе, не так ли? Вот почему, прежде чем переходить к закрытию, мы построим наши основные концепции над областью видимости в javascript.

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

program = data + behavior 

Здесь данные — это то, что хранит состояние программы во время ее выполнения. Ваша программа взаимодействует с этими данными всякий раз, когда это необходимо. Если вы немного покопаетесь, вы можете спросить себя, где они хранят эти данные? и вы можете снова ответить себе на память (неважно, что мы говорим здесь о энергозависимой или постоянной памяти, но она должна быть в памяти)!!

Однако это правильно, но вы думаете, что данные находятся в случайном месте в памяти? Ответ - нет.

Данные программы сохраняются механизмом выполнения (движок JS), и механизм JS знает, где хранить данные и как их извлекать всякий раз, когда программа запрашивает их. Существует своего рода поиск данных с использованием некоторого набора правил, и в компьютерной программе мы называем это правило разрешением области действия.

Когда вы пишете

let myVar = 2

Движок JS сохраняет это значение в каком-то заранее определенном контексте (называемом переменным окружением).

когда ты пишешь

console.log(myVar)

Он ищет в этой области и находит значение переменной myVar.

Теперь ваш мозг начал задавать больше вопросов, так как это работа мозга, если да, то давайте копать дальше.

Прежде чем запустить ваш код, за кулисами JS-движок проделывает большую работу. Это сверхмощный двигатель, который делает всю эту работу мгновенно. Мы объясним это на высоком уровне.

Всякий раз, когда запускается код Javascript, механизм JS создает контекст выполнения и внутри него запускает код. Всякий раз, когда выполняется какой-либо код JS, он выполняется внутри контекста выполнения. Вы можете думать о контексте выполнения как о среде, в которой выполняется ваш JS-код и хранятся связанные с ним данные.

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

Существует контекст выполнения, и ваш код выполняется там. Когда движок JS видит строку console.log(myVar), он ссылается на переменную среды (синяя рамка на изображении выше), и если она присутствует, то получает значение, и мы видим, что 2 печатается через console.log (мойВар).

Что могло произойти за кулисами, будем разбираться шаг за шагом.

Шаг 1) JS-движок создает контекст выполнения и объявляет некоторые предопределенные объекты (например, этот идентификатор и т. д.)

Шаг 2) JS-движок увидел строку let myVar = 2, он выделяет для нее память в переменной среды, связанной с вновь созданным контекстом выполнения.

Шаг 3) Он увидел инструкцию console.log(myVar), и ему нужно найти переменную myVar. Он выполняет какой-то поиск и получает значение из контекста выполнения, а это 2.

Что ж, возьмем другой пример, чтобы было понятнее.

function foo() {
   console.log(`value of myVar is: ${myVar}`);
}
var myVar = 3
foo(); // Invoke the foo
// output: value of myVar is: 3

Приведенный выше код снова очень прост. мы получили функцию foo и вызвали их. В тот момент, когда мы вызываем их, JS-движок создаст контекст выполнения для foo. Это будет выглядеть примерно так: -

Вы что-то наблюдали? В синем поле (переменная среда или контейнер переменных) для этого контекста выполнения ничего нет. Давайте проверим, что произошло за кулисами.

Шаг 1) JS-движок создает контекст выполнения и объявляет некоторые предопределенные объекты (например, этот идентификатор и т. д.)

Шаг 2) JS-движок увидел инструкцию, чтобы получить myVar и передать ее в console.log. Он выполняет какой-то поиск во вновь созданном контексте выполнения и ничего не находит. В JS мы используем значение undefined для присвоения, когда эта переменная еще не определена. Вы, возможно, ожидали технически, по крайней мере, с нашим обсуждением до сих пор.

Однако мы видим, что значение myVar печатается как 3. Ниже приведены выходные данные из консоли chrome dev.

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

Всякий раз, когда вы вызываете функцию, механизм JS создает совершенно новый контекст выполнения для этой функции и помещает ее в стек вызовов выполнения. Как вы можете видеть на картинке выше, у нас был глобальный контекст выполнения, где JS-скрипт начинает свое выполнение, а затем, когда мы вызывали foo (foo()), он создавал контекст выполнения и помещал его в стек вызовов. Обратите внимание, что myVar объявлен в глобальном контексте выполнения.

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

Здесь уровень глобального контекста выполнения является общим для foo, поэтому все переменные также являются общими. Это протокол разрешения области, который, если не находит переменную в текущем контексте выполнения (который запросил движок), обращается к области видимости внешнего уровня. Вот почему мы получили значение myVar из глобального контекста выполнения, хотя оно не было объявлено внутри контекста выполнения foo.

Сейчас что-то строится в твоей голове. Давайте сделаем его более сильным.

Другой пример: можете ли вы создать для этого ментальную модель?

function foo() {
    var b = 2;
    function bar(c) {
        console.log(a + b + c);
    }
    bar(3)
}
var a = 1
var b = 3
var c = 4
foo() // 6

На приведенной ниже диаграмме поясняется, что здесь происходит:

  1. Мы получили несколько переменных (a, b, c) в глобальном контексте выполнения.
  2. Мы вызвали foo, мы получили контекст выполнения foo. Внутри foo мы объявили переменную b, она переходит в переменную окружения foo (синее поле).
  3. мы вызвали функцию bar из foo, и она создала новый контекст выполнения для bar (верхний). Мы передали ему c в качестве формального параметра, поэтому JS-движок создал эту переменную в области видимости.
  4. Внутри функции bar мы печатаем сумму a, b и c.
console.log(a + b + c)

Где искать а, б, в? Из диаграммы видно, что внутри контекста исполнения бара у нас нет a, b. У нас есть только c. Где взять остальные два (а, б)? JS-движок выполняет разрешение области и обнаруживает, что мы объявили b в охватывающей области. т.е. область действия foo. Итак, он идет туда и получает значение. И мы получаем

a = 1
b = 2 // Not 3
c = 3 // Not 4

Правило. Как только разрешение области находит значение, оно прекращает поиск. Вот почему даже b объявлено в глобальном контексте выполнения, мы получили его в контексте выполнения foo и завершили поиск.

Теперь у вас может возникнуть другой вопрос: почему контекст выполнения панели сначала выполняет поиск foo в области видимости. почему он не может сначала перейти к глобальному контексту выполнения?

Ответ на этот вопрос остается в теории компиляторов. Этап выполнения JS состоит из двух частей:

  1. этап создания:- Здесь они объявляют память для требуемой переменной или функции.
  2. этап выполнения: это этап, на котором они запускают код.

На этапе создания компилятор выполняет все ссылки на основе лексического контекста. Быстрый гугл выдаст такой результат для лексики:

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

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

Если вы снова наблюдаете код: -

function foo() {
    var b = 2;
    function bar(c) {
        console.log(a + b + c);
    }
    bar(3)
}
var a = 1
var b = 3
var c = 4
foo() // 6

Функция foo объявлена ​​на глобальном уровне, функция bar объявлена ​​внутри foo. Технически мы говорим, что bar лексически ограничен внутри foo. смысл ?? Вот почему на диаграмме выше показана кривая, указывающая сначала на контекст выполнения foo, а затем контекст выполнения foo указывает на глобальный контекст выполнения.

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

Интуиция — очень мощная вещь, но всегда полагаться на нее может быть опасно.

Счастливого обучения!!!