Понять, как работает движок JavaScript

JavaScript

Переводчик: Front-end Xiaozhi

оригинал:взрыв co's.IO/ - это вопрос -Essen ...

Ставь лайк и смотри, поиск в WeChat【Переезд в мир】Обратите внимание на этого человека, который не имеет большого фабричного прошлого, но имеет восходящий и позитивный настрой. эта статьяGitHub GitHub.com/QQ449245884…Он был включен, статьи были классифицированы, и многие мои документы и учебные материалы были систематизированы.

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

некоторые существительные

JS-движок— Движок, который читает код и работает, единого «движка JS» не существует, у каждого браузера свой движок, как у Google V.

объем— «Область», из которой можно получить доступ к переменной.

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

область действия блока— диапазон, созданный фигурными скобками {}

цепочка прицелов— Функция может подняться во внешнее окружение (лексически) для поиска переменной, и она может пройти весь путь вверх, пока не достигнет глобальной области видимости.

Синхронизировать— Выполняет одну вещь за раз, «синхронный» движок выполняет одну строку за раз, JavaScript является синхронным.

асинхронный— Делайте несколько дел одновременно, проходит JSAPI браузераИмитация асинхронного поведения

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

куча— Структура данных, которая только вставляет элементы и извлекает верхний элемент. Подумайте о складывании башен в глиф; вы не можете удалить средние блоки, LIFO.

куча— Переменные хранятся в памяти.

стек вызовов— Очередь вызовов функций, которая реализует тип данных стека, что означает, что за раз может выполняться одна функция. Вызов функции помещает ее в стек, а возврат из функции извлекает ее из стека.

контекст выполнения— Среда, создаваемая JS при помещении функции в стек вызовов.

Закрытие— Когда функция создается внутри другой функции, она «запоминает» созданную ею среду при последующих вызовах.

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

Переменное продвижение— Когда переменной memory не присвоено значение, она поднимается на вершину глобальной и устанавливается вundefined.

this— Переменные/ключевые слова, автоматически создаваемые JavaScript для каждого нового контекста выполнения.

Стек вызовов

Взгляните на код ниже:

var myOtherVar = 10

function a() {
  console.log('myVar', myVar)
  b()
}

function b() {
  console.log('myOtherVar', myOtherVar)
  c()
}

function c() {
  console.log('Hello world!')
}

a()

var myVar = 5

Следует отметить несколько моментов:

  • Позиция объявления переменной (одна выше, одна ниже)

  • функцияaвызвать функцию, определенную нижеb, функция b вызывает функциюc

Что вы ожидаете, когда оно будет выполнено? возникла ли ошибка из-заbсуществуетaПосле выписки или все ок?console.logА как насчет печатных переменных?

Вот результат печати:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

Давайте разберем приведенные выше шаги выполнения.

1. Объявления переменных и функций (этап создания)

Первым шагом является выделение места в памяти для всех переменных и функций. Но учтите, что помимоundefinedКроме того, переменной не присвоено значение. следовательно,myVarЗначение при печати равноundefined, поскольку движок JS выполняет код построчно, начиная сверху.

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

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

var myOtherVar = undefined
var myVar = undefined

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

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

В глобальном контексте JS также добавляет:

  1. глобальный объект (в браузерах этоwindowобъект, в NodeJs естьglobalобъект)

  2. это указывает на глобальный объект

2. Выполнить

Далее движок JS выполняет код построчно.

myOtherVar = 10В глобальном контексте,myOtherVarназначается как10

Все функции созданы, следующим шагом является выполнение функцииa()

Каждый раз, когда вызывается функция, для нее создается новый контекст (повторите шаг 1) и помещается в стек вызовов.

function a() {
  console.log('myVar', myVar)
  b()
}

Выполните следующие действия:

  1. Создайте новый контекст функции

  2. aВнутри функции не объявлены переменные и функции.

  3. создается внутри функцииthisи указать на глобальный объект (окно)

  4. Затем обратитесь к внешней переменнойmyVar,myVarотносится к глобальной области видимости.

  5. Затем вызовите функциюb, функцияbпроцесс сaОпять же, никакого анализа здесь не делается.

Схема выполнения стека вызовов ниже:

  1. Создайте глобальный контекст, глобальные переменные и функции.

  2. Каждый вызов функции создает контекст, ссылку на внешнюю среду иthis.

  3. Функция извлекается из стека после выполнения, а контекст ее выполнения удаляется сборщиком мусора (за исключением замыканий).

  4. Когда стек вызовов пуст, он будет получать события из очереди событий.

Объем и цепочка объемов

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

функция/лексический объем

Рассмотрим следующий код:

function a() {
  var myOtherVar = 'inside A'

  b()
}

function b() {
  var myVar = 'inside B'

  console.log('myOtherVar:', myOtherVar)

  function c() {
    console.log('myVar:', myVar)
  }

  c()
}

var myOtherVar = 'global otherVar'
var myVar = 'global myVar'
a()

Необходимо отметить следующие моменты:

  1. Переменные объявляются как в глобальной области видимости, так и внутри функций.

  2. функцияcсейчас в работеbзаявление в

Результат печати следующий:

myOtherVar: "global otherVar"
myVar: "inside B"

Шаги:

  1. Глобальное создание и объявление — создает все функции и переменные в памяти, а также глобальные объекты иthis

  2. execute — читает код построчно, присваивает значения переменным и выполняетфункция

  3. функцияСоздается новый контекст и помещается в стек, в контексте создаются переменныеmyOtherVar, а затем позвонитефункция б

  4. функция бТакже создается новый контекст, который также помещается в стек

5,функциясозданный в контексте бmyVarпеременные и объявитьфункция с

Внешние ссылки, упомянутые выше, создаются с каждым новым контекстом, и внешние ссылки зависят от того, где функция объявлена ​​в коде.

  1. функция бпытаюсь напечататьmyOtherVar, но эта переменная не существует вфункция б, функция b будетИспользуйте его внешнюю ссылку, чтобы найти цепочку областей видимости. из-зафункция бобъявляется глобально, а не вфункцияобъявляется внутри, поэтому он использует глобальныйпеременная myOtherVar.

  2. функция сВыполните те же действия. из-зафункция снет самой переменнойmyVar, поэтому он просматривает цепочку областей видимости, котораяфункция б,потому чтоmyVarдафункция бВнутренняя декларация.

Ниже приведена принципиальная схема выполнения:

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

Лучше всего думать об этом как о цепочке (цепь олечения), которая может работать только в одном направлении.

  • a -> global
  • c -> b -> global

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

область действия блока

В приведенном ниже коде у нас есть две переменные и два цикла, переопределяющие одну и ту же переменную в цикле, что будет напечатано (что я все-таки делаю не так)?

function loopScope () {
  var i = 50
  var j = 99

  for (var i = 0; i < 10; i++) {}

  console.log('i =', i)

  for (let j = 0; j < 10; j++) {}

  console.log('j =', j)
}

loopScope()

распечатать результат:

i = 10
j = 99

Первая петля покрываетvar iДля разработчиков по незнанию это может вызвать ошибку.

Во втором цикле каждая итерация создает свою собственную область видимости и переменные. Это потому, что он используетletключевое слово, которое совпадает сvarто же самое, простоletИмеет собственную область действия блока. Другое ключевое словоconst, что то же самое, чтоletто же самое, ноconstПостоянная и не может быть изменена (относится к адресу памяти).

область области блока, созданная фигурными скобками {}

Давайте посмотрим на другой пример:

function blockScope () {
  let a = 5
  {
    const blockedVar = 'blocked'
    var b = 11

    a = 9000
  }

  console.log('a =', a)
  console.log('b =', b)
  console.log('blockedVar =', blockedVar)
}

blockScope()

распечатать результат:

a = 9000
b = 11
ReferenceError: blockedVar is not defined
  1. aимеет блочную область, но находится в функции, а не вложенной, в данном случае с использованиемvarэто то же самое.

  2. Для переменных с блочной областью действия он ведет себя как функция, обратите внимание, чтоvar bможно получить доступ извне, ноconst blockedVarне можем.

  3. Внутри блока нашел цепочку прицелаaи воляlet aизменить на9000.

Использование области видимости блока может сделать ваш код чище и безопаснее, и его следует использовать всегда, когда это возможно.

Цикл событий

см. далеецикл событий. Здесь работают обратные вызовы, события и API браузера.

Вещи, которые мы не обсуждали слишком много,куча,Также известен какглобальная память.它是变量存储的地方。由于了解JS引擎是如何实现其数据存储的实际用途并不多,所以我们不在这里讨论它。

Вот асинхронный код:

function logMessage2 () {
    console.log('Message 2')
}

console.log('Message 1')

setTimeout(logMessage2, 1000)

console.log('Message 3')

Приведенный выше код в основном выводит некоторые сообщения на консоль. использоватьsetTimeoutфункция задержки сообщения. Мы знаем, что js является синхронным, давайте посмотрим на вывод

Message 1
Message 3
Message 2
  1. распечатать сообщение 1

  2. вызов setTimeout

  3. распечатать сообщение 3

  4. распечатать сообщение 2

он регистрирует сообщение 3

Позже он запишет сообщение 2

setTimeoutэто API, как и большинство API браузера, когда он вызывается, он отправляет некоторые данные и обратные вызовы в браузер. Здесь мы печатаем с задержкой в ​​одну секундуMessage 2.

вызов завершенsetTimeoutПосле этого наш код продолжает работать без пауз, печатаяMessage 3и сделать то, что должно быть сделано в первую очередь.

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

пример кода

Лучший способ познакомиться с движком JS — это использовать его с некоторыми содержательными примерами.

простое закрытие

В этом примере есть функция, которая возвращает функцию и использует внешнюю переменную в возвращаемой функции, которая называетсяЗакрытие.

function exponent (x) {
  return function (y) {
   //和math.pow() 或者x的y次方是一样的
    return y ** x
  }
}

const square = exponent(2)

console.log(square(2), square(3)) // 4, 9

console.log(exponent(3)(2)) // 8

блочный код

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

function blockingCode() {
  const startTime = new Date().getSeconds()

  // 延迟函数250毫秒
  setTimeout(function() {
    const calledAt = new Date().getSeconds()
    const diff = calledAt - startTime
 
    // 打印调用此函数所需的时间
    console.log(`Callback called after: ${diff} seconds`)
  }, 250)

  // 用循环阻塞堆栈2秒钟
  while(true) {
    const currentTime = new Date().getSeconds()

    // 2 秒后退出
    if(currentTime - startTime >= 2) break
  }

}

blockingCode() // 'Callback called after: 2 seconds'

мы пытаемся250毫秒вызвать функцию после этого, но поскольку наш цикл блокирует стек, который он занимает两秒钟, поэтому функция обратного вызова фактически выполняется через две секунды.Это распространенная ошибка в приложениях JavaScript..

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

Функция задержки

когдаsetTimeoutустановлен на 0, какова ситуация?

function defer () {
  setTimeout(() => console.log('timeout with 0 delay!'), 0)
  console.log('after timeout')
  console.log('last log')
}

defer()

Вы можете ожидать, что он будет вызван немедленно, однако это не так.

Результаты:

after timeout
last log
timeout with 0 delay!

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

Кэш с замыканиями

MemoizationЭто процесс кэширования результата вызова функции.

Например, есть функция, которая складывает два числаadd. передачаadd(1,2)вернуть3, при повторном использовании тех же параметровadd(1,2)调Используйте его, на этот раз вместо пересчета запомните 1+ 2是3и вернуть соответствующий результат напрямую.MemoizationЭто может повысить скорость выполнения кода и является хорошим инструментом.

Мы можем использовать замыкания для реализации простогоmemoizeфункция.

// 缓存函数,接收一个函数
const memoize = (func) => {
  // 缓存对象
  // keys 是 arguments, values are results
  const cache = {}

  // 返回一个新的函数
  // it remembers the cache object & func (closure)
  // ...args is any number of arguments
  return (...args) => {
    // 将参数转换为字符串,以便我们可以存储它
    const argStr = JSON.stringify(args)

    // 如果已经存,则打印
    console.log('cache', cache, !!cache[argStr])

    cache[argStr] = cache[argStr] || func(...args)

    return cache[argStr]
  }
}

const add = memoize((a, b) => a + b)

console.log('first add call: ', add(1, 2))

console.log('second add call', add(1, 2))

Результаты:

cache {} false
first add call:  3
cache { '[1,2]': 3 } true
second add call 3

первый разaddметод, объект кеша пуст, он вызывает нашу входящую функцию, чтобы получить значение3.тогда это будетargs/valueПары ключ-значение хранятся в объектах кеша.

Во втором вызове значение уже находится в кеше, оно просматривается и возвращается.

дляaddДля функций наличие или отсутствие кеша кажется неактуальным и даже менее эффективным, но для некоторых сложных вычислений он может сэкономить массу времени. Этот пример — не идеальный пример кэширования, а практическое применение замыканий.

Ошибки, которые могут существовать после развертывания кода, не могут быть известны в режиме реального времени.Чтобы решить эти ошибки впоследствии, много времени тратится на отладку журнала.Кстати, я рекомендую всем полезный инструмент мониторинга ошибок.Fundebug.

общаться с

Статья постоянно обновляется каждую неделю. Вы можете выполнить поиск «Big Move to the World» в WeChat, чтобы прочитать и обновить ее как можно скорее (на одну или две статьи раньше, чем в блоге). Эта статья находится на GitHub.GitHub.com/QQ449245884…Он был включен, и многие мои документы были разобраны. Добро пожаловать в Звезду и совершенство. Вы можете обратиться в тестовый центр для ознакомления во время собеседования. Кроме того, обратите внимание на паблик-аккаунт и ответьте в фоновом режиме.Благосостояние, вы можете увидеть преимущества, вы знаете.