¿Es posible lograr un alcance dinámico en JavaScript sin recurrir a eval?


JavaScript tiene un ámbito léxico que significa que las variables no locales a las que se accede desde dentro de una función se resuelven a las variables presentes en el ámbito de los padres de esa función cuando se definió. Esto está en contraste con el ámbito dinámico en el que las variables no locales a las que se accede desde dentro de una función se resuelven a variables presentes en el ámbito de llamada de esa función cuando se llama.

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?

El programa anterior imprime 1 y luego 2 en un lenguaje con alcance léxico, e imprime 3 y luego 1 en un lenguaje de alcance dinámico. Dado que JavaScript tiene un ámbito léxico, imprimirá 1 y luego 2 como se muestra a continuación:

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

Aunque JavaScript no admite el ámbito dinámico, podemos implementarlo usando eval de la siguiente manera:

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

Me gustaría saber si existe otra forma posible de lograr el mismo resultado sin recurrir a eval.

Edit: Esto es lo que estoy tratando de implementar sin usar 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());

Sé que no es un buen ejemplo, pero la idea general es usar el ámbito dinámico para crear cierres. Creo que este patrón tiene mucho potencial.

Author: Aadit M Shah, 2012-04-08

7 answers

La búsqueda de atributos cae a través de la cadena de prototipos, que coincide bastante bien con los ámbitos dinámicos. Simplemente pase su propio entorno de variables de ámbito dinámico para usar en lugar de usar el ámbito léxico de 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
 10
Author: ephemient,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-08 19:10:02

Para agregar una nota sobre este tema:

En JavaScript cada vez que haga uso de:

  • Declaración de función declaración o expresión de definición de función entonces las variables locales tendrán Ámbito léxico .

  • Function constructor entonces las variables locales se referirán al ámbito global (código de nivel superior)

  • this es el único objeto incorporado en JavaScript que tiene una dinámica scoping y se establece a través de la ejecución (o invocación) contexto.

Así que para responder a su pregunta, En JS el this ya es característica de alcance dinámico del lenguaje e incluso no es necesario emular otro.

 12
Author: Arman McHitarian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-10 15:48:53

No lo creo.

Así no es como funciona el lenguaje. Tienes que usar algo más que variables para referirte a esta información de estado. La forma más" natural " es usar propiedades de this, supongo.

 2
Author: Thilo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-08 06:34:06

En su caso, en lugar de intentar usar el ámbito dinámico para establecer el constructor, ¿qué pasa si utiliza el valor devuelto?

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());
 2
Author: Casey Chu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-08 07:58:35

¿Por qué nadie dijo this?

Puede pasar variables del ámbito invocador a la función invocada vinculándola con un contexto.

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 );

Quizás vale la pena mencionar que en modo estricto, todas las funciones no tienen ningún "entorno" enviado a ellas por defecto (this es null). En modo no estricto el objeto global es el valor predeterminado this para una función llamada.

 2
Author: user68880,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-12 14:11:47

Puede simular el alcance dinámico utilizando variables globales, si tiene una forma de hacer azúcar sintáctica (por ejemplo, macros con gensyms) y si tiene unwind-protect.

La macro puede aparecer para volver a enlazar la variable dinámica guardando su valor en un léxico oculto y luego asignando un nuevo valor. El código unwind-protect garantiza que no importa cómo termine ese bloque, se restaurará el valor original del global.

Seudocódigo Lisp:

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

Por supuesto, esto no es un thread-forma segura de hacer alcance dinámico, pero si no estás haciendo threading, lo hará! Escondíamos detrás de una macro como:

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

Entonces, si tiene unwind-protect en Javascript y un preprocesador de macro para generar algo de azúcar sintáctica (por lo que no está codificando manualmente todos sus guardados y restauraciones protegidas por unwind), podría ser factible.

 0
Author: Kaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-08 06:34:23

Sé que esto no responde exactamente a la pregunta, pero es demasiado código para poner en un comentario.

Como un enfoque alternativo, es posible que desee buscar en la función extend de ExtJS. Así es como funciona:

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

Con propiedades públicas en lugar de variables privadas:

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;
    }
});
 0
Author: user123444555621,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-08 09:27:45