是否有可能在JavaScript中实现动态范围而无需使用eval?

时间:2012-04-08 06:26:06

标签: javascript eval dynamic-scope

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

我知道这不是一个很好的例子,但总体思路是使用动态范围来创建闭包。我认为这种模式有很大的潜力。

7 个答案:

答案 0 :(得分:12)

要添加关于此主题的说明:

在JavaScript中,只要您使用:

  • 函数声明语句或函数定义表达式,然后局部变量将具有 Lexical Scoping

  • 功能构造函数然后局部变量将引用全局范围(顶级代码)

  • this是JavaScript中唯一具有动态的内置对象 范围确定并通过执行(或调用)上下文设置。

所以要回答你的问题,在JS中, this 已经是该语言的动态范围功能,你甚至不需要模仿另一个。

答案 1 :(得分:10)

属性查找属于原型链,与动态范围非常匹配。只需传递自己的动态范围变量环境,而不是使用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

答案 2 :(得分:2)

我不这么认为。

这不是语言的运作方式。您必须使用除变量之外的其他内容来引用此状态信息。我想,最“自然”的方式是使用this的属性。

答案 3 :(得分:2)

在您的情况下,不是尝试使用动态范围设置构造函数,而是使用返回值会怎样?

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

答案 4 :(得分:2)

为什么没有人说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为空)。在非严格模式下,全局对象是被调用函数的默认this值。

答案 5 :(得分:0)

你可以使用全局变量模拟动态范围,如果你有办法做语法糖(例如带有gensyms的宏),并且你有放松保护。

宏可以通过将其值保存在隐藏的词法中然后分配新值来重新绑定动态变量。展开保护代码确保无论该块如何终止,都将恢复全局的原始值。

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中使用unwind-protect,并且在宏预处理器中生成一些语法糖(因此您不会手动打开编码所有保存和展开保护的恢复),那么它可能是可行的。

答案 6 :(得分:0)

我知道这并没有完全回答这个问题,但是发表评论的代码太多了。

作为替代方法,您可能需要查看ExtJS的extend函数。这是它的工作原理:

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