Примеры псевдокода, помогающие объяснить, что описывает официальная спецификация ECMAScript.

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

Понимание того, что такое контекст выполнения, даст вам прочную основу для понимания других понятий, таких как ключевое слово this, подъем, область действия и замыкания.

ECMAScript — это официальная спецификация JavaScript. Он описывает только реализацию того, как должен вести себя язык javascript, и примером реализации является движок Google Chrome v8. Движок javascript v8 – это Just in Time Compiler или JIT, который компилирует код javascript в машинный код во время выполнения и написан на C++. V8 также является движком javascript, который используется в Node.js.

Поэтому, когда вы смотрите на спецификацию ECMAScript, помните, что она описывает концепции языка javascript и что chrome использует движок javascript v8, реализующий то, что описывает спецификация ECMAScript.

С этим пониманием и небольшой практикой вы заметите, насколько полезной может быть спецификация, а не какой сложной она кажется.

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

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

Что такое контекст выполнения?

Контекст выполнения — это абстрактное понятие среды, в которой выполняется и оценивается ваш код. Каждый раз, когда любой код запускается в javascript, он запускается внутри контекста выполнения. Это можно проверить, используя оператор debugger в коде и просмотрев стек вызовов.

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

Типы контекста выполнения

Для простоты также существует контекст выполнения eval, но я буду говорить только о глобальном контексте выполнения и контексте выполнения функции.

Глобальный контекст выполнения

Глобальный контекст выполнения — это контекст выполнения по умолчанию, в котором выполняется весь код javascript, не находящийся внутри функции. Глобальный контекст выполнения выполняет две функции: 1. создает глобальный объект, который является окном в случае браузеров, 2. устанавливает значение this для глобального объекта. Для каждого файла javascript может быть только один глобальный контекст выполнения.

Контекст выполнения функции

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

Из спецификации:

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

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

function foo() {
  // 2. executable code associated with the running execution context (foo)
  console.log('this is the foo function')
  // 3. code that is not associated with the running execution context
  //    new execution context is created and pushed to the top of the call stack, or execution context stack
  //    control is now transferred to this executable code.
  bar()
}

function bar() {
  console.log('this is the bar function')
}

// 1. this creates a new execution context that is appended to the top of the callstack
foo()

Фазы контекста выполнения

Контекст выполнения создается каждый раз, когда вызывается функция, каждый раз, когда это происходит, есть две фазы: 1. Фаза создания и 2. Фаза выполнения.

Этап создания

На этапе создания создается контекст выполнения и создаются следующие компоненты состояния:

  • Компонент LexicalEnvironment
  • Компонент VariableEnvironment

Фаза выполнения

Здесь все назначения переменных завершены, и код может начать выполняться.

Лексическое окружение

Из спецификации:

Лексическое окружение определяет запись окружения, используемую для разрешения ссылок на идентификаторы, сделанные кодом в этом контексте выполнения.

LexicalEnvironment состоит из двух компонентов:

  1. EnvironmentRecord — сохраняет переменные и это значение
  2. [[OuterEnv]] — ссылка на внешнюю среду

Запись окружающей среды

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

Декларативная запись среды

Из спецификации:

Каждая запись декларативной среды связана с областью действия программы ECMAScript, содержащей объявления variable, constant, let, class, module, import и/или function. Декларативная запись среды связывает набор идентификаторов, определенных объявлениями, содержащимися в ее области действия.

Это означает, что каждый раз, когда делается объявление, например const someLetter = 'a' с const, let, function или любым другим из перечисленных выше, с ним связана запись декларативной среды, и она связывает идентификаторы с их ценности.

В приведенном ниже коде someLetter является идентификатором, а значение равно a.

const someLetter = 'a'
var someNumber = 1

function foo() {
  console.log('foo')
}

Объявления let и const определяют переменные, область действия которых ограничена LexicalEnvironment текущего контекста выполнения.

Концептуально это можно представить следующим образом:

GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord
      someLetter: 'a',
      foo: <ref. to foo function>
      [[ThisValue]]: GlobalObject or undefined
    },
    [[OuterEnv]]: null,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      someNumber: 1
      [[ThisValue]]: GlobalObject or undefined
    }
  }
}

Запись среды объекта

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

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

const obj = {
  age: 30
}

Наша запись среды объекта будет:

key | value
------------
age |  30
with(obj) {
  age = age + 1
}
console.log(obj) // {age: 31}
// notice how we didn't need dot or bracket notation inside the with block

with — это способ, с помощью которого мы можем создать запись среды объекта и заполнить ее ключами и значениями переданного объекта. По этой причине вы можете получить доступ к значениям без использования записи через точку. Объект окна работает так же.

Обзор

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

Существует два типа контекста выполнения:

  1. Глобальный контекст выполнения
  2. Контекст выполнения функции

Каждый контекст выполнения состоит из двух фаз:

  1. Этап создания
  2. Фаза выполнения

На этапе создания создаются следующие компоненты состояния для контекстов выполнения кода.

  • Компонент LexicalEnvironment
  • Компонент VariableEnvironment

Компонент LexicalEnvironment состоит из двух компонентов:

  1. Запись среды
  2. [[OuterEnv]] — ссылка на внешнюю среду

Нам нужно знать два типа EnvironmentRecord:

  1. Запись среды объекта (OER)
  2. Декларативная запись среды (DER)

Пример 1

let someName = 'john'
var anotherVariable = 'abc'

function greet(name) {
  return `hi ${name}`
}

greet(someName)

На фазе создания сначала создается глобальный контекст выполнения. Обратите внимание на выделенные строки: для переменных, объявленных с помощью var, установлено значение undefined, а для переменных, объявленных с помощью let или const, установлено значение uninitialized.

// creation phase
GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      someName: uninitialized,
      [[ThisValue]]: window
    }
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      anotherVariable: undefined,
      [[ThisValue]]: window
    }
  }
}

На этапе выполнения переменные будут назначены.

// execution phase
GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      someName: 'john'
      [[ThisValue]]: window
    }
    [[OuterEnv]]: null,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      anotherVariable: 'abc'
      [[ThisValue]]: window
    }
  }
}

Теперь, когда выполняется глобальный контекст выполнения, когда он сталкивается с greet(someName), создается новый контекст выполнения функции.

// creation phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      arguments: {
        0: 'john',
        length: 1
      },
      [[ThisValue]]: window
    },
    [[OuterEnv]]: GlobalLexicalEnvironment,
  },
  VariableEnvironent: {
    EnvironmentRecord: {
      Type: DeclarativeEnvironmentRecord,
      [[ThisValue]]: window
    }
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      arguments: {
        0: 'john',
        length: 1
      },
      [[ThisValue]]: window
    },
    [[OuterEnv]]: GlobalLexicalEnvironment,
  },
  VariableEnvironent: {
    EnvironmentRecord: {
      Type: DeclarativeEnvironmentRecord
      [[ThisValue]]: window 
    },
  }
}

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

Пример 2

просто чтобы донести мысль, давайте попробуем это снова с более подробным примером. Мы будем использовать этот пример из Mozilla.

function makeFunc() {
  const name = "Mozilla";

  function displayName() {
    console.log(name);
  }

  return displayName;
}

const myFunc = makeFunc()
myFunc()

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

GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      makeFunc: <ref. to makeFunc function>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: GlobalLexicalEnvironment,
  },
  VariableEnvironment: {}
}
// code executes ...

Когда глобальный контекст выполнения выполняется и достигает const myFunc = makeFunc(), создается новый контекст выполнения функции, который помещается в стек контекста выполнения.

// creation phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      arguments: {
        length: 0
      },
      name: uninitialized,
      displayName: <ref. to displayName function>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: GlobalLexicalEnvironment,
  },
  VariableEnvironment: {}
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      arguments: {
        length: 0
      },
      name: 'Mozilla',
      displayName: <ref. to displayName function>,
      [[ThisValue]]: GlobalObject or undefined
    },
    [[OuterEnv]]: GlobalLexicalEnvironment,
  },
  VariableEnvironment: {}
}

После завершения выполнения makeFunc() возвращаемое значение сохраняется внутри переменной myFunc.

Обратите внимание, что LexicalEnvironment элемента function makeFunc() остается в памяти, поскольку на его лексическое окружение ссылается его внутренняя функция displayName().

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

myFunc()

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

FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      // since there is no name here it will look into
      // outerEnv, and there name is defined
      [[ThisValue]]: GlobalObject or undefined
    },
    [[OuterEnv]]: <ref. to function makeFunc LexicalEnvironment>,
  },
  VariableEnvironment: {}
}
// code executes ...

Пример 3

Способ вызова функции определяет ее значение this. Мы проиллюстрируем два способа, как это происходит ниже с одним и тем же объектом.

const user = {
  name: 'david',
  greet() {
    console.log(`hi ${this.name}`)
  }
}

user.greet()
// global execution context pushed to stack
GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: null
  }
}

// user.greet() is encountered
// new function execution context created
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      [[ThisValue]]: <ref to user obj>
    },
    [[OuterEnv]]: <ref. to the LexicalEnvironment of GlobalExecutionContext>
  }
}

Давайте сделаем то же самое снова, потеряв this.

const user = {
  name: 'david',
  greet() {
    console.log(`hi ${this.name}`)
  }
}
const greet = user.greet
greet()
// global execution context pushed to stack
GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: null
  }
}
// greet() is encountered
// new function execution context created
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of GlobalExecutionContext>
  }
}

Пример 4

Следующий пример должен показать, как происходит поиск переменных. Что, если бы у нас была функция, которая выглядела бы так:

function a() {
  const a = 'a'
  b()
  function b() {
    const b = 'b'
    c()
    function c() {
      const c = 'c'
      console.log({a, b, c})
    }
  }
}
a()
GlobalExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: ObjectEnvironmentRecord
      [[ThisValue]]: window
    },
    [[OuterEnv]]: null
  }
}

// a() is invoked
// callstack- a
// creation phase

FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      a: uninitialized,
      b: <ref. to function b>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of GlobalExecutionContext>
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      a: 'a',
      b: <ref. to function b>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of GlobalExecutionContext>
  }
}

// b() is invoked
// callstack - a -> b
// a new function execution context is created and appended to the top of the call stack.
// now control is transferred from a to b
// creation phase

FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      b: uninitialized,
      c: <ref. to function c>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of function a>
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      b: 'b',
      c: <ref. to function c>,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of function a>
  }
}

// c() is invoked
// callstack - a -> b -> c
// creation phase

FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      c: uninitialized,
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of function b>
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      type: DeclarativeEnvironmentRecord,
      c: 'c',
      [[ThisValue]]: window
    },
    [[OuterEnv]]: <ref. to LexicalEnvironment of function b>
  }
}

Пример с циклом forEach

const person = {
  name: 'john',
  tasks: ['eat', 'clean'],
  listTasks() {
    this.tasks.forEach(task => {
      console.log(`${this.name} must ${task}`)
    })
  }
}
person.listTasks()
// creation phase
GlobalExecutionContext = {
  LexicalEnvironment: {
    ObjectEnvironmentRecord: {
      person: uninitialized
    },
    [[OuterEnv]]: null
  }
}

// execution phase
GlobalExecutionContext = {
  LexicalEnvironment: {
    ObjectEnvironmentRecord: {
      person: <ref. to object person>
    },
    [[OuterEnv]]: null
  }
}

// person.listTasks() invoked
// callstack - listTasks
// creation phase

FunctionExecutionContext = {
  LexicalEnvironment: {
    // this is a subclass of Declarative Environment Record
    FunctionEnvironmentRecord: {
      [[ThisValue]]: <ref. to user object>,
    },
    [[OuterEnv]]: <ref. to Lexical Environment of GlobalExecutionContext>
  }
}

// code executes...
// this.tasks.forEach() is invoked
// another execution context is formed and appended to the top of the stack
// callstack - listTasks -> (anonymous)

// creation phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    FunctionEnvironmentRecord: {
      [[ThisValue]]: <ref. to user object>,
      [[ThisBindingStatus]]: lexical,
      task: uninitialized
    },
    [[OuterEnv]]: <ref. to Lexical Environment of GlobalExecutionContext>
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    FunctionEnvironmentRecord: {
      [[ThisValue]]: <ref. to user object>,
      [[ThisBindingStatus]]: lexical,
      task: 'eat'
    },
    [[OuterEnv]]: <ref. to Lexical Environment of GlobalExecutionContext>
  }
}

// popped off the stack
// callstack - listTasks
// forEach runs again and creates another execution context
// callstack - listTasks -> (anonymous)

// creation phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    FunctionEnvironmentRecord: {
      [[ThisValue]]: <ref. to user object>,
      [[ThisBindingStatus]]: lexical,
      task: uninitialized
    },
    [[OuterEnv]]: <ref. to Lexical Environment of GlobalExecutionContext>
  }
}

// execution phase
FunctionExecutionContext = {
  LexicalEnvironment: {
    FunctionEnvironmentRecord: {
      [[ThisValue]]: <ref. to user object>,
      [[ThisBindingStatus]]: lexical,
      task: 'clean'
    },
    [[OuterEnv]]: <ref. to Lexical Environment of GlobalExecutionContext>
  }
}
// (anonymous) popped off the stack
// callstack - listTasks
// listTasks is finished and popped off of the stack
// global execution context popped off the stack

Это все для этой статьи. Поскольку спецификация ECMAScript на самом деле просто описывает реализацию, невозможно идеально написать это в псевдокоде, но важно понять идею, стоящую за этим, потому что движки javascript в браузере были построены на этих концепциях.