Как работает JavaScript
Первод статьи: Ionel Hindorean How JavaScript Works
Почему понимание основ бесценно
Вы, наверное, удивляетесь, почему кто-то потрудится написать длинный пост о том как работает ядро JavaScript в 2019 году.
Это потому, что я считаю, что в наши дни очень легко потеряться в экосистеме JS без глубокого понимания основ и практически невозможно исследовать более сложные темы.
Понимание того, как работает JavaScript, делает чтение и написание кода проще и менее разочаровывающим, а также позволит вам сосредоточиться на логике вашего приложения вместо того, чтобы бороться с грамматикой языка.
Как это работает?
Компьютеры не понимают JavaScript — браузеры понимают.
Помимо обработки сетевых запросов, прослушивания кликов мыши и интерпретации HTML и CSS для рисования пикселей на экране, в браузер встроен движок JavaScript.
Движок JavaScript — это программа, написанная, скажем, на C ++, которая обрабатывает весь код JavaScript, символ за символом, и «превращает» его в нечто, что ЦП компьютера может понять и выполнить — то есть в машинный код.
Это происходит синхронно, то есть по одной строке за раз и по порядку.
Он делают это потому, что машинный код сложен, и потому что инструкции машинного кода у разных производителей процессоров разные.
Таким образом, движок делают всю эту работу за разработчиков JavaScript, иначе веб-разработка была бы намного сложнее, менее популярной, и у нас не было бы таких вещей, как Medium (оригинальная статья размещена на Medium), где мы могли бы писать статьи, подобные этой (и я бы сейчас спал) ,
Движки JavaScript отличаются по типу выполнения. Он может пройти по каждой строке JavaScript и выполнять их по строчно (см. Интерпретатор), или он может быть умнее и обнаруживать такие вещи, как функции, которые часто вызываются и всегда дают один и тот же результат. Затем он может скомпилировать их в машинный код только один раз, чтобы в следующий раз, когда он встретит его, он выполнял уже скомпилированный код, что намного быстрее (см. Компиляция Just-in-time). Или он может заранее скомпилировать весь код в машинный код (см. Compiler).
Сейчас самый распространенный V8 — это такой движок JavaScript, который Google был создан в 2008 году. В 2009 году у парня по имени Райан Даль появилась идея использовать V8 для создания Node.js, среды выполнения для JavaScript вне браузера, что означало, что этот язык может также использоваться для серверных приложений.
Контекст выполнения функции
Как и любой другой язык, JavaScript имеет свои собственные правила для функций, переменных, типов данных и точных значений, которые эти типы данных могут хранить, где в коде они доступны, а где нет, и так далее.
Эти правила определяются организацией по стандартизации Ecma International, и вместе они образуют документ спецификации языка (вы можете найти последнюю версию здесь).
Таким образом, когда движок преобразует код JavaScript в машинный код, он должен учитывать спецификации.
Что если код содержит недопустимое присваивание или он пытается получить доступ к переменной, которая, согласно спецификации языка, не должна быть доступна из этой конкретной части кода?
Каждый раз, когда вызывается функция, ей нужно все это понять. Это достигается путем создания оболочки, называемой контекстом выполнения (execution contex).
Чтобы быть более конкретным и избежать путаницы в будущем, я буду называть этот контекст выполнения функции (function execution context), потому что он создается каждый раз, когда вызывается функция. Не пугайтесь этого термина и не думайте о нем слишком много сейчас, он будет подробно описано позже.
Просто помните, что он определяет такие вещи, как: «Какие переменные доступны в этой конкретной функции, какое значение this внутри нее, какие переменные и функции объявлены внутри нее?»
Глобальный контекст выполнения
Но не весь код JavaScript находится внутри функций.
Также код может быть вне любой функции на глобальном уровне, поэтому одна из самых первых вещей, которую делает движок JavaScript, — это создание глобального контекста выполнения (global execution context).
Это похоже на контекст выполнения функции и служит той же цели но на глобальном уровне, и имеет некоторые особенности.
Например, существует только один глобальный контекст выполнения, созданный в начале выполнения, внутри которого выполняется весь код JavaScript.
Глобальный контекст выполнения создает две вещи, которые специфичны для него, даже если нет кода для выполнения:
- Объект window, когда JavaScript запускается внутри браузера. Когда он запускается вне его, как в случае с Node.js, он будет чем-то вроде global. Для простоты я буду использовать window в этой статье.
- Специальную переменная которая называется this.
В глобальном контексте выполнения, и только там, this фактически равняется объекту window. Это в основном ссылка на window.
this === window // logs true
Другое тонкое различие между глобальным контекстом выполнения и контекстом выполнения функции заключается в том, что любые переменные или функции, объявленные на глобальном уровне (вне какой-либо функции), автоматически присоединяются в качестве свойств к объекту window и неявно к специальной переменной this.
Хотя функции также имеют специальную переменную this, этого не происходит в контексте выполнения функции.
Таким образом, если у нас есть глобальная переменная foo, объявленная на глобальном уровне, следующие три утверждения будут фактически указывать на нее. То же самое относится и к функциям.
foo; // 'bar' window.foo; // 'bar' this.foo; // 'bar' (window.foo === foo && this.foo === foo && window.foo === this.foo) // true
Все встроенные в JavaScript переменные и функции присоединяются к глобальному объекту window: setTimeout(), localStorage, scrollTo(), Math, fetch() и т. д. Именно поэтому они доступны в любом месте кода.
Стек исполнения (Execution Stack)
Мы знаем, что контекст выполнения функции создается каждый раз, когда вызывается функция.
Поскольку даже самые простые программы на JavaScript имеют довольно много вызовов функций, все эти контексты выполнения функций должны как-то управляться.
Посмотрите на следующий пример:
function a() { // some code } function b() { // some code } a(); b();
Когда происходит вызов функции a(), создается контекст выполнения функции, как описано выше, и выполняется код внутри функции.
Когда выполнение кода завершено (оператор return или скобка } ), контекст выполнения функции для функции a() уничтожается.
Затем происходит вызов b(), и тот же процесс повторяется для функции b().
Но такое случается редко, даже в очень простых программах JavaScript. В большинстве случаев будут функции, которые вызываются внутри других функций:
function a() { // some code b(); // some more code } function b() { // some code } a();
В этом случае создается контекст выполнения функции для a(), и прямо в середине выполнения a() встречается вызов b(). Абсолютно новый контекст выполнения функции создается и для b(), но без разрушения контекста выполнения a(), так как его код еще не полностью выполнился.
Это означает, что одновременно может существовать много контекстов выполнения функций. Тем не менее, только один из них на самом деле работает в текущий момент времени.
Чтобы отслеживать, какой из них в данный момент выполняется, используется стек, где текущий контекст выполнения функции находится на вершине стека.
Как только он завершит выполнение, он будет извлечен из стека, далее возобновится выполнение следующего контекста выполнения и так далее, пока стек выполнения не станет пустым.
Этот стек называется стеком выполнения (execution stack), представленным на рисунке ниже.
Когда стек выполнения пуст, выполняется глобальный контекст, который мы обсуждали ранее и который никогда не уничтожается, оставаясь текущим контекстом выполнения.
Очередь событий (Event Queue)
Помните, когда я говорил, что движок JavaScript — это всего лишь один компонент браузера, наряду с движком рендеринга или сетевым уровнем?
Эти компоненты имеют встроенные хуки (Hooks), с помощью которых ядро взаимодействует для инициирования сетевых запросов, рисования пикселей на экране или прослушивания щелчков мыши.
Когда вы используете что-то вроде fetch в JavaScript для выполнения HTTP-запроса, механизм фактически передает это на сетевой уровень. Всякий раз, когда приходит ответ на запрос, сетевой уровень передает его обратно движку JavaScript.
Но это может занять несколько секунд, что делает движок JavaScript во время выполнения запроса?
Просто прекращает выполнение любого кода, пока не придет ответ? Продолжает выполнение остальной части кода и, когда приходит ответ, останавливает все и выполнять обратный вызов? Или когда обратный вызов завершается, возобновляет выполнение с того места, где он остановился?
Ничего из вышеперечисленного, хотя первое может быть достигнуто с помощью await.
В многопоточных языках это может быть обработано наличием одного потока для выполнения кода в текущем контексте выполнения и другого потока для выполнения обратного вызова для события. Но это невозможно с JavaScript, так как он однопоточный.
Чтобы понять, как это на самом деле работает, давайте рассмотрим функции a() и b(), которые мы рассматривали ранее, но добавим обработчик кликов мыши и обработчик HTTP-запросов.
function a() { // some code b(); // some more code } function b() { // some code } function httpHandler() { // some code here } function clickHandler() { // some more code here } a();
Любое событие, полученное движком JavaScript от других компонентов браузера, например клик мыши или ответ по сети, не будет обработано немедленно.
Движок JavaScript может быть занят выполнением кода в этот момент, поэтому вместо этого он поместит событие в очередь, называемую очередью событий (event queue).
Мы уже говорили о стеке выполнения и о том, как контекст выполнения текущей функции извлекается из стека после завершения выполнения кода в соответствующей функции.
Затем следующий контекст выполнения возобновляет выполнение до его завершения и т. д. До тех пор, пока стек не станет пустым, а глобальный контекст выполнения не станет текущим выполняемым контекстом выполнения.
Несмотря на наличие кода для выполнения в стеке выполнения, события в очереди событий игнорируются, так как движок занят выполнением кода в стеке.
Только когда он завершится и стек выполнения станет пустым, движок JavaScript начнет обрабатывать следующее событие в очереди событий (если они там, конечно будут) и вызовит их обработчик.
Поскольку этот обработчик является функцией JavaScript, он будет обрабатываться так же, как обрабатывались a() и b(), что означает, что создается контекст выполнения функции и помещается в стек выполнения.
Если этот обработчик, в свою очередь, вызывает другую функцию, то создается другой контекст выполнения функции и поместится в стек, и так далее.
Движок JavaScript снова проверит очередь событий на наличие новых событий, только когда стек выполнения будет снова пустым.
То же самое относится к событиям клавиатуры и мыши. При клики мыши механизм JavaScript получает событие click, помещает его в очередь событий и выполняет свой обработчик только после того, как стек выполнения будет пустым.
Вы можете легко увидеть это в действии, скопировав следующий код в консоль браузера:
function documentClickHandler() { console.log('CLICK!!!'); } document.addEventListener('click', documentClickHandler); function a() { const fiveSecondsLater = new Date().getTime() + 5000; while (new Date().getTime() < fiveSecondsLater) {} } a();
Цикл while просто загрузит движок на пять секунд. Начните щелкать в любом месте документа в течение этих пяти секунд, и вы увидите, что на консоли ничего не отобразиться.
Когда пройдет пять секунд, и стек выполнения станет пустым, вызовется обработчик первого клика.
Поскольку это функция, то создается контекст выполнения функции, потом он помещается в стек, выполняется и после завершения извлечется из стека. Затем вызовется обработчик второго клика и так далее.
На самом деле, то же самое происходит с setTimeout() (и setInterval()). Обработчик, который вы предоставите для setTimeout(), фактически помещается в очередь событий. Это означает, что если вы установите тайм-аут на 0, но в стеке выполнения будет код, обработчик для setTimeout() будет вызван только тогда, когда стек будет пустым, что может произойти через множество миллисекунд.
Это одна из причин, почему setTimeout() и setInterval() не совсем точны. Скопируйте и вставьте следующий текст в консоль браузера, если вы не поверите мне на слово.
setTimeout(() => { console.log('TIMEOUT HANDLER!!!'); }, 0); const fiveSecondsLater = new Date().getTime() + 5000; while (new Date().getTime() < fiveSecondsLater) {}
Примечание. Код, помещаемый в очередь событий, называется асинхронным. Является ли это хорошим термином, это другая тема, но так ее называют люди, так что, думаю, вам нужно к этому привыкнуть.
Этапы контекста выполнения функции
Теперь, когда мы знакомы с жизненным циклом выполнения JavaScript-программы, давайте еще немного углубимся в то, как именно создается контекст выполнения функции.
Это происходит в два этапа: этап создания и этап выполнения.
Этап создания «настраивает» так, что код может быть выполнен, а этап выполнения фактически выполняет его.
На этапе создания происходят две вещи, которые очень важны:
- определяется область действия (scope).
- определяется значение this (я предполагаю, что вы уже знакомы с ключевым словом this в JavaScript).
Каждый из них подробно описан в следующих двух соответствующих разделах.
Scope и Scope Chain
Область действия состоит из переменных и функций, даже если они не были объявлены в самой функции. JavaScript имеет лексическую область видимости, что означает, что область определяется в зависимости от того, где функция объявлена в коде.
function a() { function b() { console.log(foo); // logs 'bar' } var foo = 'bar'; b(); } a();
При достижении console.log(foo), движок JavaScript сначала проверит, есть ли переменная foo в области контекста выполнения b(). Поскольку там ее нет, он перейдет в «родительский» контекст выполнения, который является контекстом выполнения a(), просто потому, что b() объявлен внутри a(). В области видимости этого контекста выполнения он найдет foo и выведет ее значение.
Если мы определим b() вне a(), вот так:
function a() { var foo = 'bar'; b(); } function b() { console.log(foo); // throws ReferenceError: foo is not defined } a();
Будет сгенерировано ReferenceError, хотя единственное различие между ними заключается в месте, где объявлено b().
«Родительская» область действия b() теперь является глобальной областью контекста выполнения, поскольку она объявляется на глобальном уровне вне какой-либо функции, и там нет переменной foo.
Я могу понять, почему это может сбивать с толку, потому что, если вы посмотрите на стек выполнения, это выглядит так:
Таким образом, легко предположить, что «родительский» контекст выполнения является следующим в стеке, ниже текущего. Однако это не так.
В первом примере контекст выполнения a() действительно является «родительским» контекстом выполнения b(). Не потому, что a() является следующим элементом в стеке выполнения, чуть ниже b(), а просто потому, что b() объявлен внутри a().
Во втором примере стек выполнения выглядит так же, но на этот раз «родительский» контекст выполнения b() является глобальным контекстом выполнения, потому что b() объявлен на глобальном уровне.
Просто помните: не важно, где вызывается функция, важно, где она объявлена.
Но что произойдет, если она не сможет найти переменную в области «родительского» контекста выполнения?
В этом случае он попытается найти его в области действия следующего «родительского» контекста выполнения, который определяется точно таким же образом.
Если его там тоже нет, он попробует следующий и т. д., пока в конце концов не достигнет глобальной области контекста выполнения. Если он не сможет найти его там, он выдаст ReferenceError.
Это называется цепочкой областей действия (scope chain), и это именно то, что происходит в следующем примере:
function a() { function b() { function c() { console.log(foo); } c(); } var foo = 'bar'; b(); } a();
Сначала он пытается найти foo в области контекста выполнения c(), затем b(), а затем, в конце концов, a(), где он его найдет.
Примечание: Помните, что движок переходит только от c() к b() и a(), потому что они объявлены внутри другого, а не потому, что их соответствующие контексты выполнения располагаются поверх другого в стеке выполнения.
Если они не будут объявлены внутри другого, тогда «родительский» контекст выполнения будет другим, как объяснено выше.
Однако, если бы внутри c() или b() была другая переменная foo, ее значение было бы выведено на консоль, потому что движок прекратит «поиск» «родительских» контекстов выполнения, как только найдет переменную.
То же самое относится и к функциям, а не только к переменным, и то же относится к глобальным переменным.
Примечание. Несмотря на то, что в приведенных выше примерах я использовал только синтаксис объявления функции, область действия и цепочка областей действия работают точно так же и для стрелочных функций, которые были представлены в ES2015 (также называемой ES6).
Замыкание (Closure)
Замыкание обеспечивает доступ к области действия внешней функции из внутренней функции.
Но это не что-то новое, я только что описал выше, как это достигается через цепочку областей действия. Что особенного в замыканиях, так это то, что, даже если код внешней функции был выполнен, контекст выполнения извлечен из стека выполнения и уничтожен, внутренняя функция все равно будет иметь ссылку на область действия внешней функции.
function a() { var name = 'John Doe'; function b() { return name; } return b; } var c = a(); c();
Это именно то, что происходит в приведенном выше примере. b() объявлена внутри a(), поэтому она может получить доступ к переменной name из области видимости a() через цепочку областей видимости.
Но она не только имеет к нему доступ, но и создает замыкание, что означает, что она может получить к нему доступ даже после возврата родительской функции a().
Переменная c является просто ссылкой на внутреннюю функцию b(), поэтому последняя строка кода фактически вызывает внутреннюю функцию b().
Вы можете прочитать больше о том, как вы можете использовать замыкания в статье на Medium, написанной Эриком Эллиоттом.
Значение this
Следующее, что определяется на этапе создания контекста выполнения, это значение this.
Я боюсь, что это не так просто, как область действия, потому что значение this внутри функции зависит от того, как вызывается функция. И, что делает этот вопрос еще более сложным, вы можете «перезаписать» поведение по умолчанию.
Я постараюсь просто объяснить, и, кстати, вы можете найти более подробную статью по этой теме на MDN.
Прежде всего, это зависит от того, как объявлена функция. С помощью объявления функций:
function a() { // ... }
Или стрелочной функций:
const a = () => { // ... };
Функция стрелок
Начну с простого. В случае стрелочных функций значение this параметра является лексическим, поэтому оно определяется аналогично тому, как определяется область действия.
«Родительский» контекст выполнения определяется точно так, как объяснено в разделе области видимости и цепочки областей видимости, в зависимости от того, где объявлена функция стрелки.
Значение this будет таким же, как значение this в родительском контексте выполнения.
Мы можем увидеть это в двух примерах ниже.
Первый из них будет записывать true, а второй — false, хотя myArrowFunction вызывается в одном и том же месте в обоих случаях. Единственное различие между ними заключается в том, где была объявлена стрелочная функция.
const myArrowFunction = () => { console.log(this === window); }; class MyClass { constructor() { myArrowFunction(); } } var myClassInstance = new MyClass(); // logs true
class MyClass { constructor() { const myArrowFunction = () => { console.log(this === window); }; myArrowFunction(); } } var myClassInstance = new MyClass(); // logs false
Поскольку значение this внутри myArrowFunction является лексическим, в первом примере это будет window, поскольку оно объявлено на глобальном уровне вне какой-либо функции или класса.
Во втором примере значение this внутри myArrowFunction будет значением this внутренней функции, которая его оборачивает.
Я расскажу о том, что именно это значение, позже в этом разделе, но пока достаточно заметить, что это не window, как в первом примере.
Помните: для функций со стрелками значение this определяется в зависимости от того, где объявлена стрелочная функция, а не где и как она вызывается.
Объявления функций
В этом случае все не так просто, и это как раз причина (или, по крайней мере, одна из них), почему стрелочные функции были введены в ES2015.
Помимо разницы в синтаксисе между функциями стрелок (const a = () => {…}) и объявлениями функций (function a () {…}), значение this является основным отличием между ними.
В отличие от стрелочных функций, значение this при обычном объявление функций не определяется лексически в зависимости от того, где объявлена функция. Оно определяется в зависимости от того, как вызывается функция. И есть несколько способов вызвать функцию:
- Простой вызов:
myFunction()
- Вызов метода объекта:
myObject.myFunction()
- Вызов конструктора:
new myFunction()
- Вызов обработчика событий DOM:
document.addEventListener(‘click’, myFunction)
Значение this внутри myFunction() определяется по-разному для каждого из этих типов вызовов, независимо от того, где объявлена функция myFunction(), поэтому давайте рассмотрим их один за другим и посмотрим, как это работает.
Простой вызов
function myFunction() { return this === window; // true } myFunction();
Простой вызов — это просто вызов функции, как в примере выше: само имя функции, без каких-либо предшествующих символов, за которым следует скобки () (конечно, с любыми дополнительными аргументами внутри).
В случае простого вызова значение this внутри функции всегда является глобальным this, которое, в свою очередь, указывает на объект глобального window.
Но помните, это верно только для простого вызова; за именем функции следует ().
Примечание. Поскольку значение this в простом вызове функции на самом деле является ссылкой на объект глобального window, использование this внутри функций, которые должны вызываться простым вызовом, считается плохой практикой. Это потому, что любые свойства, прикрепленные к this внутри функции, на самом деле присоединяются к объекту window и становятся глобальными переменными, что является плохой практикой.
Вот почему в строгом режиме (strict mode) значение this внутри любой функции, вызываемой простым вызовом undefined, и в приведенном выше примере будет выведено значение false.
Вызов метода объекта
const myObject = { myMethod: function() { return this === myObject; // true } }; myObject.myMethod();
Когда свойство объекта объявлена как функция в качестве значения, оно считается методом этого объекта, отсюда и термин вызов метода (method invocation).
Когда используется этот тип вызова, значение this внутри функции будет просто указывать на объект, для которого вызывается метод, который в примере выше является myObject .
Примечание. Если бы использовался синтаксис стрелочных функций, вместо объявления функции, значением this функции внутри был бы объект window.
Это потому, что его родительский контекст выполнения был бы глобальным контекстом выполнения. Тот факт, что он объявлен внутри объекта, ничего не меняет.
Вызов конструктора
Другой способ вызвать функцию — это добавить к вызову ключевое слово new, как в примере ниже.
Когда функция вызывается таким образом, функция возвращает новый объект (даже если у него нет оператора return), и значение this внутри функции будет указывать на этот вновь созданный объект.
Объяснение немного упрощено (подробнее тут в MDN).
function MyConstructorFunction() { this.a = 1; } const myObject = new MyConstructorFunction(); // a new object is created // inside MyConstructorFunction(), "this" points to the newly created onject, // so it should have a property "a". myObject.a; // 1
Примечание. То же самое относится и к использованию ключевого слова new в классе, поскольку class на самом деле являются синтаксический сахар.
Примечание. Стрелочные функции нельзя использовать в качестве конструктора.
Вызов обработчика событий DOM
document.addEventListener('click', DOMElementHandler); function DOMElementHandler() { console.log(this === document); // true }
Когда функция вызывается как обработчик события DOM, значением this внутри функции будет элемент DOM, в который было помещено событие.
Примечание: обратите внимание, что во всех других типах вызовов мы сами вызываем функцию. В случае с обработчиками событий, не мы передаем ссылку на функцию обработчика. Движок JavaScript вызывает функцию, и мы не можем контролировать, как он это делает.
Вызов с пользовательским значением this
Значение this внутри функции может быть явно установлено, вызывая функцию с помощью bind(), call() или apply() из Function.prototype.
const obj = {}; function a(param1, param2) { return [this === window, this === obj, param1, param2]; } a.call(obj, 1, 2); // [false, true, 1, 2] a.apply(obj, [3, 4]); // [false, true, 3, 4] a.bind(obj)(5, 6); // [false, true, 5, 6] a(7, 8); // [true, false, 7, 8]
В приведенном выше примере показано, как работает каждый из них.
call() и apply() очень похожи, единственное отличие состоит в том, что с apply() аргументы функции передаются в виде массива.
Хотя call() и apply() фактически вызывают функцию со значением this с тем что вы передаете в качестве первого аргумента, bind() не вызывает функцию.
Вместо этого он возвращает новую функцию, которая в точности соответствует функции, с которой была использована bind(), но со значением this равным тому, что вы передаете в качестве аргумента для bind().
Вот почему вы видите (5, 6) после a.bind (obj), чтобы фактически означает вызов функции, возвращаемую bind().
В случае bind() значение this внутри возвращаемой функции постоянно связано с тем, что вы передаете в качестве значения this (отсюда и имя bind()).
Независимо от того, какой тип вызова используется, значение this внутри возвращаемой функции всегда будет тем, которое было предоставлено в качестве аргумента. Его можно изменить только снова с помощью call(), bind() или apply().
Но есть и исключение из правил, и это исключение является вызовом конструктора. При вызове функции таким способом, помещая ключевое слово new перед вызовом, значением this внутри функции всегда будет объект, возвращаемый вызовом, даже если новой функции было дано другое this с помощью bind().
Вы можете проверить это в следующем примере:
function a() { this.three = 'three'; console.log(this); } const customThisOne = { one: 'one' }; const customThisTwo = { two: 'two' }; const bound = a.bind(customThisOne); // returns a new function with the value of this bound to customThisOne bound(); // logs customThisOne bound.call(customThisTwo); // logs customThisOne, even though customThisTwo was passed to .call() bound.apply(customThisTwo); // same as above new bound(); // logs the object returned by the new invocation, bypassing the .bind(customThisOne)
Вот пример того, как вы будете использовать bind() для управления значением this для обработчика события click, который мы обсуждали ранее:
const myCustomThis = {}; document.addEventListener('click', DOMElementHandler.bind(myCustomThis)); function DOMElementHandler() { console.log(this === document); // false (used to be true before bind() was used) console.log(this === myCustomThis); // true }
Примечание: bind(), call() и apply() не могут быть использованы для передачи пользовательского значения this стрелочным функциям.
Примечание о стрелочных функциях
Теперь вы можете видеть, как эти правила для объявлений функций, хотя и довольно простые, могут вызвать путаницу из-за всех особых случаев и стать источником ошибок.
Небольшое изменение в том, как вызывается функция, изменит значение this внутри нее. Это может вызвать цепную реакцию, и поэтому важно знать эти правила и то, как они могут повлиять на ваш код.
Вот почему люди, которые пишут спецификации для JavaScript, придумали стрелочные функции, где значение this всегда лексическое и всегда определяется одинаково, независимо от того, как они вызываются.
Поднятие (Hoisting)
Я упоминал ранее, что когда вызывается функция, движок JavaScript сначала проходит через код, выясняет область действия и значение this, и идентифицирует переменные и функции, объявленные в теле функции.
На этом первом этапе (этапе создания) эти переменные получают специальное значение undefined, независимо от того, какое фактическое значение им назначено в коде.
Только на втором этапе (этапе исполнения) им присваивается фактическое значение, и это происходит только при достижении строки назначения.
Вот почему следующий код JavaScript выведет undefined:
console.log(a); // undefined var a = 1;
На этапе создания переменная a идентифицируется, и ей присваивается специальное значение undefined.
Затем, на этапе выполнения, достигается строка, которая выводит на консоль a
значение undefined, так как она было установлено в качестве значения на предыдущем шаге.
Когда будет достигнута строка, где ей назначено значение 1, значение a изменится на 1, но значение undefined уже было выведено в консоли.
Этот эффект называется подъемом (hoisting), как если бы все объявления переменных были подняты в верхнюю часть кода. Как видите, это не совсем то, что происходит, но именно этот термин, используется для его описания.
Примечание: это также происходит со стрелочными функциями, но не с обычными функциями. На этапе создания функциям не назначается специальное значение undefined, а вместо этого все тело функции помещается в память. Вот почему функция может быть вызвана еще до ее объявления, как в примере ниже, и она будет работать.
a(); function a() { alert("It's me!"); }
Примечание. При попытке получить доступ к переменной, которая вообще не была определена, выдается ReferenceError: x is not defined. Таким образом, существует разница между «undefined» и «not defined», что может немного сбивать с толку.
Заключение
Я помню, как читал статьи о поднятие, области видимости, замыканиях и т. д., И все они имели смысл, когда я их читал, но когда я всегда сталкивался с каким-то странным поведением JavaScript, я просто не мог это объяснить Проблема заключалась в том, что я всегда читал о каждой концепции по отдельности, по одной за раз. Поэтому я попытался дать общую картину как работает движок JavaScript. Как создаются контексты выполнения и помещаются в стек выполнения, как работает очередь событий, как определяется this и область действия и т. д. После этого мне стало проще обнаруживать потенциальные проблемы, быстрее искать источники ошибок и я стал чувствовать себя намного увереннее в программирование в целом.
Я надеюсь, что эта статья сделает то же самое для вас!
Ссылки
- Programming JavaScript Applications
- JavaScript: Understanding the Weird Parts (первые три с половиной часа бесплатно здесь )
- You don’t know JS
Огонь статья!
Благодарю за труд!
«Проблема заключалась в том, что я всегда читал о каждой концепции по отдельности, по одной за раз. Поэтому я попытался дать общую картину как работает движок JavaScript. Как создаются контексты выполнения и помещаются в стек выполнения, как работает очередь событий, как определяется this и область действия и т. д.» — так точно! Еще раз благодарю )
Спасибо! Все описано очень доходчиво.
сохранил себе в закладки. автору огромный респект, идеально расписано для новичков