Read this article in: English, Chinese, French.
Введение
В этой заметке мы затронем контексты исполнения JavaScript и связанные с ними типы исполняемого кода.
Определения
Итак, каждый раз, когда происходит передача управления исполняемому коду ECMAScript-программы, осуществляется вход в контекст исполнения.
Контекст исполнения (Execution context, сокращённо — EC) – это абстрактное понятие, используемое спецификацией ECMA, для типизации и разграничения исполняемого кода.
Стандарт не задаёт чётких рамок на структуру и вид EC с точки зрения реализации; это задача JavaScript-движков, реализующих стандарт.
Логически, совокупность контекстов исполнения представляет собой стек. Дно этого стека — всегда глобальный контекст, верхушка — текущий (активный) контекст исполнения. Стек модифицируется (наполняется/очищается) по мере входа в различные виды EC.
Типы исполняемого кода
С абстрактным понятием контекста исполнения, связано понятие типа исполняемого кода. Говоря о типе кода, можно, в определённых моментах, подразумевать контекст исполнения.
Для примеров, обозначим стек контекстов исполнения в виде массива:
ECStack = [];
Стек наполняется каждый раз при входе в функцию (даже, если функция вызвана рекурсивно, или, в качестве конструктора), а также, при работе функции eval
.
Глобальный код (Global Code)
Это тип кода, обрабатываемый на уровне Program
: загруженный js
-файл или inline-код (между тегами <script></script>
). Данный тип не включает в себя тех частей кода, которые находятся в телах функций.
При инициализации (запуске программы), ECStack
имеет вид:
ECStack = [ globalContext ];
Код функции (Function Code)
При передаче управления функции (всем видам функций), ECStack
наполняется новыми элементами. Стоит отметить, что код конкретной функции не включает в себя коды вложенных функций. Для примера, возьмём функцию, вызывающую себя рекурсивно один раз:
(function foo(bar) { if (bar) { return; } foo(true); })();
Тогда, ECStack модифицируется следующим образом:
// первый запуск foo ECStack = [ <foo> functionContext globalContext ]; // рекурсивный запуск foo ECStack = [ <foo> functionContext - рекурсивно <foo> functionContext globalContext ];
Каждый возврат из функции завершает текущий контекст исполнения, и ECStack
модифицируется соответствующим образом: отработавшие контексты удаляются последовательно и в обратном порядке (вполне естественная организация стека). После отработки данного кода, ECStack
снова содержит лишь globalContext
— вплоть до завершения программы.
Брошенное, но не пойманное исключение, также может завершать один или более контекстов:
(function foo() { (function bar() { throw 'Выход из контекстов bar и foo'; })(); })();
Eval
Code
С кодом eval
‘а — интересней. В данном случае, присутствует понятие вызывающего контекста (calling context), т.е. контекста, из которого вызвана функция eval
. Модификации, производимые eval
‘ом (например, объявление переменной или функции), воздействуют на вызывающий контекст:
eval('var x = 10'); (function foo() { eval('var y = 20'); })(); alert(x); // 10 alert(y); // "y" is not defined
В строгом режиме ES5, eval
больше не воздействует на вызывающий контекст. Вместо этого код исполняется в локальной песочнице.
Для примера выше мы имеем следующую модификацию ECStack
:
ECStack = [ globalContext ]; // eval('var x = 10'); ECStack.push({ context: evalContext, callingContext: globalContext }); // отработал eval ECStack.pop(); // вызов функции foo ECStack.push(<foo> functionContext); // eval('var y = 20'); ECStack.push({ context: evalContext, callingContext: <foo> functionContext }); // отработал eval ECStack.pop(); // return из foo ECStack.pop();
Т.е. вполне обычный и логичный Call-стек.
eval
вторым параметром можно было передавать вызывающий контекст. Таким образом, можно было воздействовать на приватные переменные:
function foo() { var x = 1; return function () { alert(x); }; } var bar = foo(); bar(); // 1 eval('x = 2', bar); // указываем контекст, меняем внутренний var "x" bar(); // 2
Однако в целях безопасности в современных движках эта особенность была удалена.
Заключение
Данный теоретический минимум необходим для дальнейшего разбора объектов, связанных с контекстами исполнения (таких как Объект переменных (Variable object), Цепь областей видимости (Scope chain) и т.д., описание которых можно найти в соответствующих заметках).
Список литературы
Соответствующий раздел спецификации ECMA-262-3 — 10. Контексты исполнения.
Автор: Dmitry Soshnikov
Дата публикации: 26.06.2009
кстати, везде кроме ИЕ можно проводить вот таки операции с контекстом при помощи eval, вызывая её для другого объекта.
Или даже вот такой вариант:
@Nekromancer
Во втором примере объявляется глобальная переменная test.
Вы забыли поставить точку с запятой после определения первой анонимной функции (которая сохраняется в переменную myFn). В итоге, вся эта конструкция означает запуск этой первой анонимной функции, с передачей ей в качестве параметра — результата выполнения второй анонимной функции.
Т.е.:
Но значением параметра для первой функции будет всё равно undefined, т.к. именно его неявно возвращает вторая функция (поскольку нет явного return‘a).
А в момент выполнения второй функции переменной myFn ещё не присвоена первая функции, и поэтому, myFn пока равен undefined (проверьте alert‘ом перед eval‘ом во второй функции).
Поэтому Ваш
eval.call(mtFn, "var test = 'my value'");
равносиленeval.call(undefined, "var test = 'my value'");
. Что равносильно (по алгоритму call)eval.call(window, "var test = 'my value'");
.Поэтому переменная test будет доступна в глобальном контексте.
И всё это из-за забытой точки с запятой 😉
Dmitry.
объявили myFn, а в call передаем mtFn 😉
Дмитрий, объясните пожалуйста этот момент:
А именно: в стэк добавляем 2 элемента, а удаляем один.
Bdiang
Да, это я просто не совсем удачно выбрал обозначение (путаница с реальной функцией
push
массивов). Предполагается один элемент с двумя свойствами — сам контекст, и вызывающий контекст (callingContext или — caller):Дмитрий, вы не могли бы пояснить последний код, а именно:
Происходит возврат анонимной функции. Функция обращается к переменной ‘x’, которая инициализирована в foo. мне не понятно следующее. При выходе из foo, локалькая переменная x должна быть утерена, так как при выходе из контекста, он исчезает со стека. Связь с переменной должна быть утеряна. Тем не менее bar() дает результат = 1.
Не могли бы вы пояснить, как происходит передача значения?
@serega
Да, это было бы верно, если бы сами данные (переменные) хранились бы на стеке. Однако на стеке в языках с замыканиями хранится именно этот контекст исполнения. В ES3 контекст имеет вид:
Данные хранятся в объекте переменных (VO) — в данном случае для
foo
—{x: 10, arguments: ...}
. А сам объект переменных хранится в куче — и хранится он там до тех пор, пока на него есть хоть одна ссылка.В момент активации
foo
таких ссылки две — изES3Context
и из функцииbar
, т.к.bar
при создании запоминает (в своем свойстве[[Scope]]
)ScopeChain
родительского контекта, т.е. контекстаfoo
(и мы видим, чтоScopeChain
foo
содержит VO, где хранитсяx
):Дальше, по выходу из
foo
, контекст удаляется со стека, но, т.к. самиVO
иScopeChain
хранятся на куче (а на стеке — только ссылки на них), тоScopeChain
остается жить, т.к. на него есть ссылка изbar
.В общем, все это управляется GC, в зависимости, остались ли ссылки на структуры или нет.
Собственно, это и есть
замыкание
, когда функцияbar
замкнула родительский контекст и он остался жить.Подробней: ES3.Ch6.Замыкания и ES3.Ch4.Scope Chain. Или же наиболее полный вариант: ES5.Ch3.1.Lexical Environments.
Спасибо, за ответ, все стало на свои места.
Да, это я просто не совсем удачно выбрал обозначение (путаница с реальной функцией push массивов)
Наверное использование push в примере и после Вашей правки не совсем корректно. push ведь добавляет в конец массива (стека) , а добавление д.б. в начало стека.
pop – удаляет последний элемент массива , это тоже выпадает из рамок корректного объяснения.
Надо как-то исхитриться и описать по другому, все ведь привыкли к работе методов Array (push и pop). Хотя смысл примеров понятен.
@arbeiter
Нет, все верно. Стек — “последний пришел” (добавился “сверху” в конец,
push
), первым ушел (удалился тоже “сверху” черезpop
).(Часто фигурирует альтернативная фразировка: “Первым пришел, последним ушел” — FILO)
Спасибо , теперь понятно.
А разве понятие вызывающего контекста отсутствует для обычной (пользовательской) функции , чтобы это особо выделять ?
Т.е. например переменная определенная в eval остается доступной в вызывающем контексте и после завершения работы eval т.е. помещается в VO-объект вызывающего контекста ?
@arbeiter
В данном случае это выделяется, чтобы показать, что
eval
воздействует на вызывающий контекст.Обычная функция, как Вы правильно отметили тоже имеет вызывающий контекст, но, к примеру, объявленный
var
внутри функции не создает переменную в вызывающем контексте, как это делаетeval
.Да, именно так (за исключением, как было отмечено, strict-mode’a ES5).
Добрый день. Можете пояснить почему здесь нужна точка с запятой?
Что значит передаётся как параметр? Мы же не вызываем
myFn
.И почему код выше даёт другой результат в сравнении с
Хех, любопытно, есть ли какая-то хитрая возможность выполнить eval применительно к вызывающему контексту вложенной функции объекта внутри IIFE в обход ‘use strict’;
Вот пытаюсь придумать, как это обыграть в виде примерно следующем:
грубо говоря, я желаю с помощью ajax->json выцепить асинхронно строку кода, который нужно выполнить в текущем контексте objFunc для того чтобы добавить в этот же объект свойство или метод ( method_3 ).
Есть идеи? 😉
У меня есть идея с new Worker(), но это уже HTML 5 …
Дмитрий, с теперешней главной странички Вашего сайта исчезла эта глава (Тонкости ECMA-262-3. Часть 1. Контексты исполнения).
Доступ к ней возможен только из ссылок. С чем это связано или я возможно ошибаюсь ?
@Arbeiter, вроде все на месте 🙂