Можно ли достичь динамической области видимости в JavaScript, не прибегая к eval?

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

x=1
function g () { echo $x ; x=2 ; }
function f () { local x=3 ; g ; }
f # does this print 1, or 3?
echo $x # does this print 1, or 2?

Вышеупомянутая программа печатает 1, а затем 2 на языке с лексической областью видимости, и она печатает 3, а затем 1 на языке с динамической областью видимости. Поскольку JavaScript имеет лексическую область видимости, он будет печатать 1, а затем 2, как показано ниже:

var print = x => console.log(x);

var x = 1;

function g() {
    print(x);
    x = 2;
}

function f() {
    var x = 3;
    g();
}

f();           // prints 1

print(x);      // prints 2

Хотя JavaScript не поддерживает динамическую область видимости, мы можем реализовать его с помощью eval следующим образом:

var print = x => console.log(x);

var x = 1;

function g() {
    print(x);
    x = 2;
}

function f() {
    // create a new local copy of `g` bound to the current scope
    // explicitly assign it to a variable since functions can be unnamed
    // place this code in the beginning of the function - manual hoisting
    var g_ = eval("(" + String(g) + ")");
    var x = 3;
    g_();
}

f();                         // prints 3

print(x);                    // prints 1

Я хотел бы знать, существует ли другой способ добиться того же результата, не прибегая к eval.

Изменить: вот что я пытаюсь реализовать без использования eval:

var print = x => console.log(x);

function Class(clazz) {
    return function () {
        var constructor;
        var Constructor = eval("(" + String(clazz) + ")");
        Constructor.apply(this, arguments);
        constructor.apply(this, arguments);
    };
}

var Rectangle = new Class(function () {
    var width, height;

    constructor = function (w, h) {
        width = w;
        height = h;
    };

    this.area = function () {
        return width * height;
    };
});

var rectangle = new Rectangle(2, 3);
print(rectangle.area());

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


person Aadit M Shah    schedule 08.04.2012    source источник
comment
Красиво написанный, интересный вопрос. Хотя он требует объективного ответа, но я подозреваю, что он вызовет множество споров и субъективных ответов о том, почему этого нельзя или не следует делать, если кто-то не покажет, как это можно сделать. Что привело вас к этому вопросу?   -  person Chris Wesseling    schedule 08.04.2012
comment
@ChrisWesseling - Я обновил свой вопрос, чтобы показать, что заставило меня опубликовать его. Вышеупомянутая программа отлично работает и работает на всех платформах. Я считаю, что у него большой потенциал для создания шаблонов классов и многого другого. Широкая публика должна использовать его с умом. Возможно, это одна из единственных веских причин использовать eval.   -  person Aadit M Shah    schedule 08.04.2012
comment
Причина, по которой я хочу использовать динамическую область видимости, заключается в том, что я могу вводить частные переменные в область действия функции. Например, в моей вышеупомянутой программе я также могу передать переменную с именем uber, которая указывает на родителя данного класса. Эта переменная должна быть доступна для класса, но не должна быть доступна для публики. Следовательно, я не могу просто установить его в экземпляре класса и назвать его днем. Таким образом, обходной путь.   -  person Aadit M Shah    schedule 08.04.2012
comment
аналогичный вопрос: stackoverflow.com/questions/10031399   -  person user123444555621    schedule 08.04.2012
comment
Подойдет ли вам приведенный ниже код? var x = 1; функция g () {печать (this.x); this.x = 2; } функция f () {var x = 3; this.g (); } print (x);   -  person Ken Russell    schedule 11.03.2013
comment
@RajkumarMasaniayan - Нет. Боюсь, что ваш код ужасно запутан. Функция g не может получить доступ к локальным переменным f, если она не определена в f. Вот почему я сделал var g = eval(String(g)). В вашем случае это похоже на мой первый пример выше (без динамической области видимости). Хуже того, вы явно устанавливаете глобальные переменные с помощью this.   -  person Aadit M Shah    schedule 11.03.2013
comment
@Aadit - Следующий код делает то, что вы хотите, но я не уверен, достаточно ли он универсален и обрабатывает все крайние случаи. Уточняю здесь, на всякий случай. var test = function () {this.x = 1; вар г = функция г () {console.log (this.x); this.x = 2; } var f = функция f () {this.x = 3; g.call (это); } this.f = f; новый f (); console.log (this.x); } новый тест ();   -  person Ken Russell    schedule 11.03.2013
comment
@RajkumarMasaniayan - Нет, он не делает то, что я хочу. Я хочу иметь возможность изменять частные переменные, а не свойства this. Я почти уверен, что использование eval - единственный способ сделать это. Однако, как и любой программист, я должен был научиться принимать язык, а не бороться с ним. Так что мне больше не нужна динамическая область видимости. Я нашел лучшее решение своей проблемы в функциональном программировании и использовал свои новые знания для создания очень небольшой и эффективной библиотеки JavaScript для объектно-ориентированного и функционального программирования - augment. знак равно   -  person Aadit M Shah    schedule 11.03.2013


Ответы (7)


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


// Polyfill for older browsers.  Newer ones already have Object.create.
if (!Object.create) {
  // You don't need to understand this, but
  Object.create = function(proto) {
    // this constructor does nothing,
    function cons() {}
    // and we assign it a prototype,
    cons.prototype = proto;
    // so that the new object has the given proto without any side-effects.
    return new cons();
  };
}

// Define a new class
function dyn() {}
// with a method which returns a copy-on-write clone of the object.
dyn.prototype.cow = function() {
  // An empty object is created with this object as its prototype.  Javascript
  // will follow the prototype chain to read an attribute, but set new values
  // on the new object.
  return Object.create(this);
}

// Given an environment, read x then write to it.
function g(env) {
  console.log(env.x);
  env.x = 2;
}
// Given an environment, write x then call f with a clone.
function f(env) {
  env.x = 3;
  g(env.cow());
}

// Create a new environment.
var env = new dyn();
// env -> {__proto__: dyn.prototype}
// Set a value in it.
env.x = 1;
// env -> {x: 1}  // Still has dyn.prototype, but it's long so I'll leave it out.

f(env.cow());
// f():
//   env -> {__proto__: {x: 1}}  // Called with env = caller's env.cow()
//   > env.x = 3
//   env -> {x: 3, __proto__: {x: 1}}  // New value is set in current object
//   g():
//     env -> {__proto__: {x: 3, __proto__: {x: 1}}}  // caller's env.cow()
//     env.x -> 3  // attribute lookup follows chain of prototypes
//     > env.x = 2
//     env -> {x: 2, __proto__: {x: 3, __proto__: {x: 1}}}

console.log(env.x);
// env -> {x: 1}  // still unchanged!
// env.x -> 1
person ephemient    schedule 08.04.2012
comment
Ваш код очень запутанный. Не хотите давать содержательные комментарии, чтобы объяснить, что вы делаете? - person Aadit M Shah; 08.04.2012
comment
@AaditMShah Прокомментировал. И прочтите Введение в объектно-ориентированный JavaScript. - person ephemient; 08.04.2012
comment
Хорошо, я наконец понял ваш код. Вы используете конструктор с именем dyn для имитации глобальной области видимости. Каждая функция имеет формальный параметр env, который эквивалентен объекту активации этой функции. Этот объект env предоставляется вызывающей стороной. Для функции f мы предоставляем глобальный экземпляр dyn. Для g мы предоставляем экземпляр глобального dyn копирования при записи, помещая первый в цепочку прототипов. Это интуитивно понятный ответ, и поиск в области видимости действительно напоминает обход цепочки прототипов. Плюс один за поиск альтернативы вместо использования eval. знак равно - person Aadit M Shah; 09.04.2012
comment
Спасибо, что поделились этой ссылкой со мной - хотя я не узнал из нее ничего, чего раньше не знал, это был хороший жест. Оценил. - person Aadit M Shah; 09.04.2012
comment
@AaditMShah Рад помочь. На самом деле, моя первая мысль (с использованием with для расширения области действия) не не работает ... и это к лучшему, так как это ужасная особенность Javascript :) - person ephemient; 09.04.2012
comment
Я принимаю этот ответ, поскольку он напрямую отвечает на мой вопрос. Однако я по-прежнему предпочитаю использовать eval, а не передавать объект активации. Слишком много бухгалтерии. Думаю, это один из тех случаев, когда использование eval действительно не является злом, и преимущества моделирования динамических осциллографов затмевают любые потери производительности. При разумном использовании это не окажет негативного влияния на остальную часть кода. Тем не менее, спасибо за вашу помощь. знак равно - person Aadit M Shah; 11.04.2012

Чтобы добавить примечание по этой теме:

В JavaScript всякий раз, когда вы используете:

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

  • Функция, тогда локальные переменные будут ссылаться на глобальную область видимости (код верхнего уровня)

  • this - единственный встроенный объект в JavaScript, который имеет динамическую область видимости и устанавливается через контекст выполнения (или вызова).

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

person Arman    schedule 10.05.2013
comment
Я благодарен за информацию this. Спасибо. - person Aadit M Shah; 11.05.2013

Я так не думаю.

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

person Thilo    schedule 08.04.2012

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

function Class(clazz) {
    return function () {
        clazz.apply(this, arguments).apply(this, arguments);
    };
}

var Rectangle = new Class(function () {
    var width, height;

    this.area = function () {
        return width * height;
    };

    // Constructor
    return function (w, h) {
        width = w;
        height = h;
    };
});

var rectangle = new Rectangle(2, 3);
console.log(rectangle.area());
person Casey Chu    schedule 08.04.2012
comment
Я мог бы это сделать, но, как я объяснил в своем последнем комментарии к моему вопросу, использование динамической области видимости позволяет мне вводить столько переменных, сколько я хочу, в область действия функции. Вы можете вернуть только одно значение. - person Aadit M Shah; 08.04.2012

Почему никто не сказал this?

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

function called_function () {
   console.log(`My env ${this} my args ${arguments}`, this, arguments);
   console.log(`JS Dynamic ? ${this.jsDynamic}`);
}

function calling_function () {
   const env = Object.create(null);
   env.jsDynamic = 'really?';

   ... 

   // no environment
   called_function( 'hey', 50 );

   // passed in environment 
   called_function.bind( env )( 'hey', 50 );

Возможно, стоит упомянуть, что в строгом режиме все функции по умолчанию не отправляют "окружение" (this имеет значение null). В нестрогом режиме глобальный объект - это значение по умолчанию this для вызываемой функции.

person user68880    schedule 12.06.2015
comment
Люди уже сказали this: Арман, Тило. Если задуматься, this - это просто еще один аргумент. Итак, вам тоже не нужно this. Просто используйте дополнительный параметр. Это именно то, что ephemient описывает в своем ответе, и поэтому я принял его. Ваш ответ на самом деле не добавляет никакой ценности. - person Aadit M Shah; 12.06.2015
comment
Я думаю, вы могли это видеть именно так. - person user68880; 14.06.2015

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

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

Псевдокод Lisp:

(let ((#:hidden-local dynamic-var))
  (unwind-protect
    (progn (setf dynamic-var new-value)
           body of code ...)
    (set dynamic-var #:hidden-local)))

Конечно, это не поточно-ориентированный способ создания динамической области видимости, но если вы не выполняете многопоточную обработку, это подойдет! Мы бы спрятали это за макросом вроде:

(dlet ((dynamic-var new-value))
   body of code ...)

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

person Kaz    schedule 08.04.2012
comment
Не могли бы вы рассказать попроще? Может, связать меня с интернет-ресурсом? - person Aadit M Shah; 08.04.2012
comment
Я обновил комментарий иллюстрацией кода с использованием Lisp; надеюсь, это поможет. - person Kaz; 08.04.2012
comment
Думаю, мне лучше начать изучать Лисп сейчас. Спасибо за помощь. Оценил. знак равно - person Aadit M Shah; 08.04.2012
comment
Похоже, защита JavaScript от размотки try { ... } finally { ... }. Таким образом, вы должны установить новое значение в блоке try, а затем восстановить переменную до ее сохраненного значения в finally. В JavaScript нет макросов, но можно использовать какой-нибудь препроцессор преобразования текста в текст. Кодеры JavaScript в любом случае используют такие вещи, например компилятор Closure, который сжимает код JavaScript. - person Kaz; 08.04.2012
comment
Это не вариант. Препроцессоры макросов работают во время компиляции. Мне нужно решение во время выполнения. - person Aadit M Shah; 08.04.2012

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

В качестве альтернативного подхода вы можете изучить функцию extend в ExtJS. Вот как это работает:

var Rectangle = Ext.extend(Object, {
    constructor: function (w, h) {
        var width = w, height = h;
        this.area = function () {
            return width * height;
        };
    }
});

С общедоступными свойствами вместо частных переменных:

var Rectangle = Ext.extend(Object, {
    width: 0,
    height: 0,  

    constructor: function (w, h) {
        this.width = w;
        this.height = h;
    },

    area: function () {
        return this.width * this.height;
    }
});
person user123444555621    schedule 08.04.2012