Мотивация

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

Вы когда-нибудь догадывались, что у переменной было заданное значение, а оказалось другое?

Вы когда-нибудь пытались понять «это» в JavaScript?

Позвольте мне провести вас через основную концепцию, которая прояснит ситуацию. Это модель оценки среды.

Какова модель среды оценки?

Проще говоря, это то, как язык программирования (его компилятор или интерпретатор) оценивает выражения. В частности, как он находит значение для данной переменной, используемой в выражении.

Например, чтобы вычислить это простое выражение `x + y`, он должен найти значения для `x` и `y`, а затем применить к ним оператор сложения.

Я считаю, что тот факт, что переменная может иметь разные значения в разных контекстах, понятен всем инженерам-программистам. Здесь мы пытаемся понять, как языки программирования поддерживают эти значения для различных контекстов.

Как работает модель оценки среды?

Для простоты давайте обозначим модель оценки среды как EME и будем использовать эту аббревиатуру в остальной части статьи.

EME моделирует контексты как среды. Любое выражение может иметь значение только при оценке в данной среде. Окружение — это место, которое следует проверить, чтобы найти значение для переменной.

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

Достаточно разговоров, давайте посмотрим на пример JS и визуализируем его базовую среду.

const sum = (x, y) => x + y

const main = () => {
  const num1 = 1
  const num2 = 2
  const result = sum(num1, num2)
  console.log(`result = ${result}`)
}

main()

Это очень простая программа, которая:

1- определяет переменную `sum` как функцию, которая суммирует 2 переданных аргумента

2- определяет основную переменную как функцию, которая определяет две переменные для суммирования, вызывает функцию sum, передавая эти переменные в качестве аргументов, присваивая результат переменной с именем `result`, а затем записывает его в консоль.

3- Наконец, он вызывает функцию `main`.

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

1- Он создает привязку для переменной `sum` в глобальной среде со значением тела функции и формальными параметрами (в данном случае `x` и `y`).

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

3- Он вызывает функцию `main`. Это представит новую среду, окружающая среда которой является глобальной. Затем он интерпретирует `main` тело, добавляя привязки для num1: 1`, `num2: 2` и `result:`.

Но чтобы получить значение переменной `result`, это представит новую среду для функции `sum`

Обратите внимание, что формальные параметры `sum`, такие как `x` и `y`, определены здесь как привязки со значениями переданных аргументов. Самое важное, на что здесь следует обратить внимание, это то, что окружающая среда `sum` является глобальной средой, а не `main`. Это связано с тем, что среда, созданная путем вызова функции, определяется тем, где функция была определена, а не тем, где функция была вызвана. Это чрезвычайно важно, как мы увидим в следующем примере.

Когда интерпретатор пытается вычислить выражение `x + y`, он будет искать привязки `x` и `y` в текущей среде, и если он не сможет их найти, он будет искать их во внешней среде и так далее. до тех пор, пока он либо не найдет их, либо не сообщит об ошибке, либо не обработает их как «неопределенные» или «нулевые». Это деталь реализации, основанная на интерпретаторе. Но важной концепцией здесь является иерархия окружений и то, как это влияет на значения переменных. В этом случае интерпретатор найдет привязки для этих переменных в текущей среде и будет использовать значения, которые будут оценивать выражение как «3».

Сложный пример

Теперь вы понимаете идею EME. Давайте возьмем сложный пример и посмотрим, как это понимание может помочь вам лучше угадывать:

var x = 5

const print = () => {
  console.log(`x = ${x}`)
}

function main() {
  var x = 10
  print()
}

main()

Эта программа:

1- определяет переменную `x` со значением `5`

2- определяет переменную `print` с функцией значения, которая регистрирует переменную `x`

3- определяет переменную `main` с функцией значения, которая определяет переменную `x` со значением `10`, а затем вызывает функцию `print`

4- вызывает функцию `main`

Мы можем визуализировать базовую среду, используя эту диаграмму.

Теперь, как вы думаете, что будет выведено на консоль? `x = 5` или `x = 10`? Найдите время, чтобы подумать об этом со своим старым пониманием и с новым пониманием EME.

Удивительно, но правильный ответ `x = 5`! Почему это? Потому что при вызове `print` новая среда, которая будет создана, будет иметь окружающую среду как глобальную, а не `main`. Именно здесь была определена функция `print`. Когда EME попытается найти привязку для `x` в среде `print`, он ничего не найдет, а затем направится в окружающую среду, которая является глобальной. Глобальная среда имеет привязку для `x` со значением `5`.

А что, если мы изменим программу следующим образом:

var x = 5

const print = () => {
  console.log(`x = ${x}`)
}

function main() {
  x = 10
  print()
}

main() 

Мы просто удалили «var» из выражения «var x = 10», чтобы оно стало «x = 10». Это меняет вывод программы? Найдите минутку и подумайте об этом.

Ответ — да, выход новой программы будет «x = 10» вместо «x = 5». Почему это? Поскольку `var x = 10` создает новую привязку в среде `main`, которая не влияет на среду `print`. Однако `x = 10` присваивает новое значение переменной `x`, которая определена в глобальной среде, и, следовательно, это повлияет на привязку, используемую `print`.

Теперь вернемся к старой версии программы перед удалением `var`. Что, если бы мы определили `print` внутри `main`, а не глобально

var x = 5

function main() {
  const print = () => {
    console.log(`x = ${x}`)
  }
  
  var x = 10
  
  print()
}

main()

Программа по-прежнему печатает `x = 5`? Ответ - нет. Это связано с тем, что теперь окружающая среда `print` является `main`.

Таким образом, когда EME ищет привязку, она получит привязку в `main`, которая равна `x: 10`, прежде чем достигнет глобальной среды.