Как переменные являются выделенной памятью в JavaScript?

Я хотел бы знать, как локальные переменные являются выделенной памятью в JavaScript. В C и C++ локальные переменные хранятся на стеке. Действительно ли это - то же в JavaScript? или все хранится в "куче"?

44
задан subbu 10 May 2010 в 05:16
поделиться

1 ответ

На самом деле это очень интересная область JavaScript, и есть по крайней мере два ответа:

  • Ответ с точки зрения того, что определяет спецификация, и
  • Ответ с точки зрения того, что на самом деле делают движки JavaScript, которые могут быть оптимизированы (и часто так и есть)

С точки зрения спецификации: Способ обработки локальных переменных в JavaScript сильно отличается от того, как это делает C. Когда вы вызываете функцию, помимо всего прочего, создается лексическое окружение для этого вызова, которое называется запись окружения. Для простоты я буду называть их оба вместе "объектом связывания" (в спецификации есть веская причина, по которой они разделены; если вы хотите разобраться в этом глубже, выделите несколько часов и прочитайте спецификацию). Объект binding содержит bindings для аргументов функции, всех локальных переменных, объявленных в функции, и всех функций, объявленных внутри функции (наряду с парой других вещей). привязка - это комбинация имени (например, a) и текущего значения для привязки (вместе с парой флагов, о которых нам здесь не нужно беспокоиться). Неквалифицированная ссылка внутри функции (например, foo в foo, но не foo в obj.foo, которая является квалифицированной) сначала проверяется на объект привязки, чтобы узнать, соответствует ли она привязке на нем; если да, то используется эта привязка. Когда закрытие переживает возврат функции (что может произойти по нескольким причинам), объект связывания для этого вызова функции сохраняется в памяти, поскольку закрытие имеет ссылку на объект связывания в том месте, где оно было создано. Итак, в терминах спецификации все дело в объектах.

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

Вот пример:

function foo(a, b) {
    var c;

    c = a + b;

    function bar(d) {
        alert("d * c = " + (d * c));
    }

    return bar;
}

var b = foo(1, 2);
b(3); // alerts "d * c = 9"

Когда мы вызываем foo, создается объект связывания с такими связями (согласно спецификации):

  • a и b - аргументы функции
  • c - локальная переменная, объявленная в функции
  • bar - функция, объявленная внутри функции
  • (. ...и несколько других вещей)

Когда foo выполняет оператор c = a + b;, он ссылается на привязки c, a и b объекта привязки для этого вызова foo. Когда foo возвращает ссылку на объявленную внутри нее функцию bar, bar переживет вызов foo с возвратом. Поскольку bar имеет (скрытую) ссылку на объект связывания для этого конкретного вызова foo, объект связывания выживает (тогда как в обычном случае на него не было бы невыполненных ссылок, и поэтому он был бы доступен для сборки мусора).

Позже, когда мы вызываем bar, создается new объект связывания для этого вызова, содержащий (среди прочего) связывание под названием d - аргумент для bar. Этот новый объект привязки получает объект привязки parent: тот, который присоединен к bar. Вместе они образуют "цепочку областей видимости". Неквалифицированные ссылки внутри bar сначала проверяются на объект привязки для вызова bar, поэтому, например, d разрешается в d на объект привязки для вызова bar. Но неквалифицированная ссылка, которая не соответствует привязке этого объекта привязки, затем проверяется на родительском объекте привязки в цепочке областей видимости, которым является объект привязки для вызова foo, который создал bar. Поскольку у него есть привязка для c, именно эта привязка используется для идентификатора c в bar. Например, в грубой форме:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
|   global binding object   |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| ....                      |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `foo` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| a = 1                     |
| b = 2                     |
| c = 3                     |
| bar = (function)          |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `bar` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| d = 3                     |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+

Интересный факт: Эта цепочка областей видимости - то, как работают глобальные переменные в JavaScript. Обратите внимание на "глобальный объект привязки" в приведенном выше примере. Поэтому в функции, если вы используете идентификатор, которого нет в объекте связывания для данного вызова функции, и нет ни в одном из других объектов связывания между этим и глобальным объектом связывания, если глобальный объект связывания имеет для него привязку, то используется глобальная привязка. Вуаля, глобальные переменные. (В ES2015 это стало немного интереснее благодаря наличию двух слоев в объекте глобального связывания: слой, используемый старомодными глобальными объявлениями, такими как var и объявлениями функций, и слой, используемый более новыми, такими как let, const и class. Разница в том, что более старый слой также создает свойства глобального объекта, к которым вы получаете доступ через window в браузерах, а более новый слой этого не делает. Так что глобальное объявление let не создает свойства window, а глобальное объявление var создает.)

Реализации вольны использовать любой механизм, который они хотят скрыть под обложкой, чтобы вышеупомянутое казалось имело место. Невозможно получить прямой доступ к объекту связывания для вызова функции, и спецификация ясно дает понять, что совершенно нормально, если объект связывания является просто концепцией, а не буквальной частью реализации. Простая реализация вполне может просто буквально делать то, что сказано в спецификации; более сложная реализация может использовать стек, когда нет закрытий (для повышения скорости), или всегда использовать стек, но затем "отрывать" объект связывания, необходимый для закрытия, когда стек раскрывается. Единственный способ узнать в каждом конкретном случае - посмотреть на их код. :-)

Больше о замыканиях, цепочке областей видимости и т.д. здесь:

54
ответ дан 26 November 2019 в 22:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: