in ECMAScript

Тонкости ECMA-262-3. Часть 1. Контексты исполнения.

Read this article in: English, Chinese (version 1, version 2), Arabic, Japaneses, Korean.

В этой заметке мы затронем контексты исполнения JavaScript и связанные с ними типы исполняемого кода.

Итак, каждый раз, когда происходит передача управления исполняемому коду ECMAScript-программы, осуществляется вход в контекст исполнения.

Контекст исполнения (Execution context, сокращённо — EC) – это абстрактное понятие, используемое спецификацией ECMA, для типизации и разграничения исполняемого кода.

Стандарт не задаёт чётких рамок на структуру и вид EC с точки зрения реализации; это задача JavaScript-движков, реализующих стандарт.

Логически, совокупность контекстов исполнения представляет собой стек. Дно этого стека — всегда глобальный контекст, верхушка — текущий (активный) контекст исполнения. Стек модифицируется (наполняется/очищается) по мере входа в различные виды EC.

С абстрактным понятием контекста исполнения, связано понятие типа исполняемого кода. Говоря о типе кода, можно, в определённых моментах, подразумевать контекст исполнения.

Для примеров, обозначим стек контекстов исполнения в виде массива:

ECStack = [];

Стек наполняется каждый раз при входе в функцию (даже, если функция вызвана рекурсивно, или, в качестве конструктора), а также, при работе функции eval.

Это тип кода, обрабатываемый на уровне Program: загруженный js-файл или inline-код (между тегами <script></script>). Данный тип не включает в себя тех частей кода, которые находятся в телах функций.

При инициализации (запуске программы), ECStack имеет вид:

ECStack = [
  globalContext
];

При передаче управления функции (всем видам функций), 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‘а — интересней. В данном случае, присутствует понятие вызывающего контекста (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-стек.

В старых версиях SpiderMonkey (Firefox), вплоть до версии 1.7, в функцию 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

Write a Comment

Comment

17 Comments

  1. кстати, везде кроме ИЕ можно проводить вот таки операции с контекстом при помощи eval, вызывая её для другого объекта.

    (function(){
    	eval.call(window, "var test = 'my value'");
    }());
    alert(test);

    Или даже вот такой вариант:

    var myFn = function(){
    	alert(test);
    }
    (function(){
    	eval.call(mtFn, "var test = 'my value'");
    }());
  2. @Nekromancer

    Во втором примере объявляется глобальная переменная test.

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

    Т.е.:

    var myFn = function(){
        alert(test);
    }(function(){ .... }()); // <-- вызов первой функции

    Но значением параметра для первой функции будет всё равно 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.

  3. объявили myFn, а в call передаем mtFn 😉

  4. Дмитрий, объясните пожалуйста этот момент:

    // eval('var x = 10');
    ECStack.push(
      evalContext,
      callingContext: globalContext
    );
     
    // отработал eval
    ECStack.pop();
    

    А именно: в стэк добавляем 2 элемента, а удаляем один.

  5. Bdiang

    в стэк добавляем 2 элемента, а удаляем один

    Да, это я просто не совсем удачно выбрал обозначение (путаница с реальной функцией push массивов). Предполагается один элемент с двумя свойствами — сам контекст, и вызывающий контекст (callingContext или — caller):

    ECStack.push({
      context: evalContext,
      caller: globalContext
    });
  6. Дмитрий, вы не могли бы пояснить последний код, а именно:

    Происходит возврат анонимной функции. Функция обращается к переменной ‘x’, которая инициализирована в foo. мне не понятно следующее. При выходе из foo, локалькая переменная x должна быть утерена, так как при выходе из контекста, он исчезает со стека. Связь с переменной должна быть утеряна. Тем не менее bar() дает результат = 1.

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

  7. @serega

    При выходе из foo, локалькая переменная x должна быть утерена

    Да, это было бы верно, если бы сами данные (переменные) хранились бы на стеке. Однако на стеке в языках с замыканиями хранится именно этот контекст исполнения. В ES3 контекст имеет вид:

    ES3Context = {
      VO / AO: { ... } // ссылка на объект переменных,
      ScopeChain: VO + все родительские VO,
      This: this
    };

    Данные хранятся в объекте переменных (VO) — в данном случае для foo{x: 10, arguments: ...}. А сам объект переменных хранится в куче — и хранится он там до тех пор, пока на него есть хоть одна ссылка.

    В момент активации foo таких ссылки две — из ES3Context и из функции bar, т.к. bar при создании запоминает (в своем свойстве [[Scope]]) ScopeChain родительского контекта, т.е. контекста foo (и мы видим, что ScopeChain foo содержит VO, где хранится x):

    bar.[[Scope]] = foo.context.ScopeChain;

    Дальше, по выходу из foo, контекст удаляется со стека, но, т.к. сами VO и ScopeChain хранятся на куче (а на стеке — только ссылки на них), то ScopeChain остается жить, т.к. на него есть ссылка из bar.

    В общем, все это управляется GC, в зависимости, остались ли ссылки на структуры или нет.

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

    Подробней: ES3.Ch6.Замыкания и ES3.Ch4.Scope Chain. Или же наиболее полный вариант: ES5.Ch3.1.Lexical Environments.

  8. Спасибо, за ответ, все стало на свои места.

  9. Да, это я просто не совсем удачно выбрал обозначение (путаница с реальной функцией push массивов)

    Наверное использование push в примере и после Вашей правки не совсем корректно. push ведь добавляет в конец массива (стека) , а добавление д.б. в начало стека.

    pop – удаляет последний элемент массива , это тоже выпадает из рамок корректного объяснения.

    Надо как-то исхитриться и описать по другому, все ведь привыкли к работе методов Array (push и pop). Хотя смысл примеров понятен.

  10. @arbeiter

    Наверное использование push в примере и после Вашей правки не совсем корректно. push ведь добавляет в конец массива (стека) , а добавление д.б. в начало стека.

    pop – удаляет последний элемент массива , это тоже выпадает из рамок корректного объяснения.

    Нет, все верно. Стек — “последний пришел” (добавился “сверху” в конец, push), первым ушел (удалился тоже “сверху” через pop).

    (Часто фигурирует альтернативная фразировка: “Первым пришел, последним ушел” — FILO)

  11. Спасибо , теперь понятно.

  12. С кодом eval‘а — интересней. В данном случае, присутствует понятие вызывающего контекста (calling context), т.е. контекста, из которого вызвана функция eval.

    А разве понятие вызывающего контекста отсутствует для обычной (пользовательской) функции , чтобы это особо выделять ?

    Модификации, производимые eval‘ом (например, объявление переменной или функции), воздействуют на вызывающий контекст

    Т.е. например переменная определенная в eval остается доступной в вызывающем контексте и после завершения работы eval т.е. помещается в VO-объект вызывающего контекста ?

  13. @arbeiter

    А разве понятие вызывающего контекста отсутствует для обычной (пользовательской) функции , чтобы это особо выделять ?

    В данном случае это выделяется, чтобы показать, что eval воздействует на вызывающий контекст.

    Обычная функция, как Вы правильно отметили тоже имеет вызывающий контекст, но, к примеру, объявленный var внутри функции не создает переменную в вызывающем контексте, как это делает eval.

    Модификации, производимые eval‘ом (например, объявление переменной или функции), воздействуют на вызывающий контекст

    Да, именно так (за исключением, как было отмечено, strict-mode’a ES5).

  14. Добрый день. Можете пояснить почему здесь нужна точка с запятой?

    var myFn = function(){
        alert(test);
    } // здесь
    (function(){
        eval.call(mtFn, "var test = 'my value'");
    }());

    Что значит передаётся как параметр? Мы же не вызываем myFn.
    И почему код выше даёт другой результат в сравнении с

    var myFn = function(){
        alert(test);
    } // здесь
    (function(){
        eval.call(mtFn, "var test = 'my value'");
    })();
  15. Хех, любопытно, есть ли какая-то хитрая возможность выполнить eval применительно к вызывающему контексту вложенной функции объекта внутри IIFE в обход ‘use strict’;

    Вот пытаюсь придумать, как это обыграть в виде примерно следующем:

    (function(){
    
    var objFunc = {
     mathod_1: function() { eval('trolo-lo-some-code') },
     mathod_2: function() { return },
    }
    
    })();

    грубо говоря, я желаю с помощью ajax->json выцепить асинхронно строку кода, который нужно выполнить в текущем контексте objFunc для того чтобы добавить в этот же объект свойство или метод ( method_3 ).

    Есть идеи? 😉

    У меня есть идея с new Worker(), но это уже HTML 5 …

  16. Дмитрий, с теперешней главной странички Вашего сайта исчезла эта глава (Тонкости ECMA-262-3. Часть 1. Контексты исполнения).

    Доступ к ней возможен только из ссылок. С чем это связано или я возможно ошибаюсь ?