Полное руководство: подъем переменных, область видимости и замыкания

JavaScript
  • Оригинальный автор:Tyler McGinnis
  • Оригинальная ссылка:Tyler McGinnis.com/ultimate - так что…
  • Для некоторых ссылок в тексте может потребоваться лестница.
  • Критика и исправления приветствуются.

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

Одной из стратегий программирования является разделение кода. Хотя эти дизассемблированные «части» имеют разные названия (функции, методы, пакеты и т. д.), все они существуют для одной цели — уменьшить сложность приложения и упростить управление им. Теперь отбросьте мысли разработчика и представьте, что вы являетесь движком Javascript, который анализирует код В этом случае мы можем использовать ту же стратегию разделения кода, что и при написании кода.Разобрать код? Оказывается, можем, и эти «части» называются контекстами выполнения.Точно так же, как функции/модули/пакеты могут помочь вам в сложной разработке, контексты выполнения помогают механизмам Javascript управлять всем сложным процессом анализа и выполнения кода.. Итак, теперь, когда мы понимаем назначение контекста выполнения, возникает следующий вопрос: как создается контекст выполнения? Из чего они сделаны?

Когда механизм Javascript запускает код, первый создаваемый контекст выполнения называется «глобальным контекстом выполнения». Первоначально глобальный контекст состоит из этих двух битов:глобальный объектс однимthisПеременная. это относится к глобальному объекту.Если вы запускаете Javascript в браузере, то этот глобальный объектwindowОбъект, если вы работаете в среде Node, этот глобальный объектglobalобъект.

Как видно из изображения выше, даже без кода все еще естьwindowа такжеthis. Это самый основной глобальный контекст выполнения.

Посмотрим, что произойдет с добавленным кодом:

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

На этапе создания глобального контекста выполнения механизм Javascript:

1. 创建一个全局对象;
2. 创建this对象;
3. 给变量和函数分配内存;
4. 给变量赋默认值undefined,把所有函数声明放进内存。

До этапа выполнения механизм Javascript запускает ваш код построчно и выполняет их.

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

На этапе создания,windowа такжеthisсоздается, для объявлений переменных устанавливается значение по умолчанию undefined, а все объявления функций сохраняются в памяти. Оказавшись на этапе выполнения, движок Javascript начинает выполнять код построчно, присваивая реальные значения переменным, которые уже существуют в памяти.

Анимация действительно крутая, но она не настолько хороша, чтобы ты тапнул по ней и сам прочувствовал процесс. Вам нужен инструмент, поэтому я создалJavascript Visualizer. Если вы хотите просмотреть код в примере, вы можете использоватьэта ссылка.

Чтобы действительно закрепить концепцию создания и выполнения, давайте выведем некоторые значения в консоль после создания и перед выполнением:

console.log('name: ', name)
console.log('handle: ', handle)
console.log('getUser :', getUser)

var name = 'Tyler'
var handle = '@tylermcginnis'

function getUser () {
  return {
    name: name,
    handle: handle
  }
}

В приведенном выше коде, как вы думаете, консоль выведет какие результаты? Когда движок запускал код Javascript построчно и вызывалconsole.log(), фаза создания уже произошла. Это означает, что, как мы видели ранее, объявлению переменной уже присвоено значение по умолчанию undefined, а объявление функции уже готово в памяти. В примереnameа такжеhandleЗначениеundefined,getUserЭто также ссылка на функцию в памяти.

console.log('name: ', name) // name: undefined
console.log('handle: ', handle) // handle: undefined
console.log('getUser :', getUser) // getUser: ƒ getUser () {}

var name = 'Tyler'
var handle = '@tylermcginnis'

function getUser () {
  return {
    name: name,
    handle: handle
  }
}

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

И измените имя переменной на другую строку, напечатанный результат будет следующим

Процесс присвоения значений по умолчанию объявлениям переменных на этапе создания называетсяпеременное продвижение.

Вы чувствуете просветление? Возможно, прежнее понимание переменного продвижения было не очень ясным. Что вас смущает в подъеме переменных, так это то, что никто на самом деле не «поднимается» и не перемещается. Теперь, когда вы понимаете контекст выполнения, что объявлениям переменных присваиваются значения по умолчанию на этапе создания, вы понимаете «поднятие», потому что это именно то, что оно означает буквально.


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

Повторим ключевой момент: контекст выполнения создается только тогда, когда движок Javascript впервые начинает синтаксический анализ кода (соответствующий глобальному контексту выполнения) или когда вызывается функция.

Главный вопрос, который нам нужно сейчас выяснить, заключается в том, в чем разница между глобальным контекстом выполнения и контекстом выполнения функции. Напомним, что ранее мы узнали, что на этапе глобального создания движок Javascript:

1. 创建一个全局对象;
2. 创建this对象;
3. 给变量和函数分配内存;
4. 给变量赋默认值undefined,把所有函数声明放进内存。

Теперь переключитесь на контекст выполнения функции, подумайте, какой шаг имеет неверный номер? Да, это первый шаг. Нам достаточно иметь один глобальный объект, и именно тот, который создается на этапе создания глобального контекста выполнения, а не при каждом вызове функции. То, что должно быть создано в контексте выполнения функции, должно бытьargumentsобъекта, поэтому при создании контекста выполнения функции движок Javascript будет:

1.Создать глобальный объект

1. Создайте объект аргументов;
2. Создать этот объект;;
3. Выделить память для переменных и функций;
4. Присвойте переменной значение по умолчанию undefined и поместите все объявления функций в память.

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

Нажмите здесь, чтобы попрактиковаться

Как мы сказали, при вызовеgetUser, создается новый контекст выполнения. существуетgetUserЭтап создания этапа создания контекста выполнения, механизм Javascript создаетthisобъект иargumentsобъект.getUserТам нет никаких переменных, поэтому движку Javascript не нужно снова выделять память или делать «подъем».

Вы могли заметить, что когдаgetUserПосле выполнения функции она начинается сПосмотретьисчез в. Фактически, механизм Javascript создает нечто, называемое «стеком выполнения» (также называемым стеком вызовов). Каждый раз, когда вызывается функция, создается новый контекст выполнения, который добавляется в стек вызовов; каждый раз, когда функция завершает работу, она извлекается из стека вызовов. Поскольку Javascript является однопоточным, черезJavascript VisualizerКак видите, каждый новый контекст выполнения вложен в другой, образуя стек вызовов.


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

Нажмите здесь, чтобы попрактиковаться

Здесь следует отметить несколько важных деталей.первый, все параметры, переданные функции, существуют как локальные переменные в контексте выполнения этой функции. В примереhandleсуществует одновременно с глобальным контекстом выполнения иgetURLконтекст выполнения, потому что мы передали его вgetURLфункционировать как параметр.Второй, переменные, объявленные в функции, существуют в контексте выполнения функции. Итак, когда мы создаемtwitterURL, он будет существовать вgetURLв контексте выполнения. Это может показаться очевидным, но это основа нашей следующей темы — масштаб.


Может быть, вы слышали об этом раньшеобъемОпределение «где доступна переменная». Как бы вы это ни понимали в то время, объедините свои новые знания сJavascript VisualizerИнструменты, концепция масштаба станет для вас более ясной. MDN определяет область действия как «текущий контекст выполнения». Звучит знакомо? Мы можем думать о области действия как о том, «где доступны переменные», поскольку мы понимаем контекст выполнения.

Вот небольшой тест. В приведенном ниже коде напечатанныйbarЧто это будет?

function foo(){
    var bar='Declared in foo';
}
foo();
console.log(bar);

Давайте взглянем на визуализатор Javascript:

Javascript Visualizer

когда мы звонимfoo, который добавляет контекст выполнения в стек вызовов. На этапе создания было произведеноthis,arguments,barустановлен наundefined. Затем на этапе выполнения поместите строку'Declare in foo'наделятьbar. На этом этап выполнения заканчивается.fooКонтекст выполнения извлекается из стека вызовов.fooПосле того, как он всплывает, код запускается для печатиbarв раздел консоли. На данный момент, согласноJavascript Visualizerотображаемое состояние,barвообще не существует, так что мы получаемundefined. (Примечание переводчика: На самом деле запуск этого примера выдаст ошибку:Uncaught ReferenceError: bar is not defined) Это говорит нам о том, что область действия переменной, созданной в функции, является локальной. Это означает (обычно исключения будут описаны позже), что после удаления контекста выполнения функции из стека вызовов переменные, объявленные в этой функции, становятся недоступными.

Давайте посмотрим на другой пример. Что выведет консоль после выполнения кода?

function first(){
    var name='Jordyn';
    console.log(name);
}
	
function second(){
    var name='Jake';
	console.log(name);
}
	
console.log(name);
	
var name='Tyler';
first();
second();
console.log(name);

Нажмите здесь, чтобы попрактиковаться

Консоль распечатаетundefined,Jordyn,Jake,Tyler. Вы можете думать об этом так: каждый новый контекст выполнения имеет свою собственную переменную среду. Даже если есть другие контексты выполнения, содержащие переменныеname, механизм Javascript по-прежнему будет запускаться из текущего контекста выполнения.

Это поднимает вопрос, а что, если в текущем контексте выполнения нет переменной для поиска? Остановится ли Javascript на этом? Ответ в примере ниже.

var name='Tyler';
function logName(){
	console.log(name);
}
logName();

Нажмите здесь, чтобы попрактиковаться

Ваша интуиция может быть такой: поскольку вlogNameне был найден в контексте выполненияnameпеременная, которая обязательно распечатаетundefined. вообще-то нет. Если механизм Javascript не может найти подходящую локальную переменную в контексте выполнения функции, он будет искать в ближайшем родительском контексте. Эта цепочка поиска простирается до глобального контекста выполнения. Если в этот момент переменная все еще не найдена, механизм Javascript выдаст ошибку ссылки.

Всякий раз, когда требуемая переменная не найдена в текущем контексте выполнения, механизм Javascript выполняет поиск по уровням вверх.作用域链. Визуализатор Javascript описывает цепочку областей видимости, представляя каждый контекст выполнения в виде области другого цвета с иерархическим отступом. Вы можете интуитивно понять, что дочерний контекст выполнения может ссылаться на переменные, объявленные в родительском контексте выполнения, но не наоборот.


Ранее мы узнали, что переменные, созданные в функциях, действительны только локально, и как только контекст выполнения функции извлекается из стека вызовов, эти переменные недоступны (обычно). Настало время рассмотреть ситуации, выходящие за рамки «обычно». Если вы встраиваете функцию внутрь другой функции, возникает исключение. В этом случае вложенности функций, даже если контекст выполнения родительской функции извлекается из стека вызовов, дочерняя функция все равно может получить доступ к области действия родительской функции. Куча многословия, давайте взглянем на Javascript Visualizer.

var count=0;
function makeAdder(x){
    return function inner(y){
        return x+y;
    }
}
var add5=makeAdder(5);
count+=add5(2);

Нажмите здесь, чтобы попрактиковаться

Уведомление,makeAdderПосле извлечения контекста выполнения из стека вызовов визуализатор Javascript создаетClosure Scope(闭包作用域).Closure Scopeпеременные среды иmakeAdderПеременная среда в контексте выполнения такая же. Это потому, что мы встраиваем другую функцию в функцию. В этом примереinnerфункция, встроенная вmakeAdder, такinnerсуществуетmakeAdderсоздал переменную среду на основеЗакрытие.因为闭包作用域的存在,即使makeAdderбыл извлечен из стека вызовов,innerвсе еще есть доступ кxпеременные (через цепочку областей видимости).

Как вы могли догадаться, такого рода дочерние функции «закрыты» в переменной среды своей родительской функции (Примечание переводчика: Первоначальноa child function “closing” over the variable environment of its parent function) понятие, которое называетсяЗакрытие.


Раздел благосостояния

Вот некоторые связанные темы, и я знаю, что если я не упомяну об этом, кто-нибудь придет и добавит это.

глобальная переменная

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

В средах браузера и узла, если вы не объявляете (например, используетеvar/let/const) напрямую создает переменную, которая также станет свойством глобального объекта.

// In the browser
var name = 'Tyler'

function foo () {
  bar = 'Created in foo without declaration'
}

foo()

console.log(window.name) // Tyler
console.log(window.bar) // Created in foo without declaration

пусть и const

нажмите здесь, чтобы посмотреть видео

this

В этой статье мы узнали, что на этапе создания каждого контекста выполнения механизм Javascript создаетthisОбъект. Если вы хотите узнать больше об этом, я рекомендую вам прочитатьWTF is this - Understanding the this keyword, call, apply, and bind in JavaScript.