【问题标题】:Is it possible to achieve dynamic scoping in JavaScript without resorting to eval?是否可以在不使用 eval 的情况下在 JavaScript 中实现动态范围?
【发布时间】:2012-04-21 02:19:39
【问题描述】:

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

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

【问题讨论】:

  • 写得很好,有趣的问题。虽然它要求一个客观的答案,除非有人展示它是如何做到的,我怀疑它会引发很多争论和主观答案,为什么它不能或不应该做。是什么让你想到这个问题的?
  • @ChrisWesseling - 我更新了我的问题以显示是什么让我发布了它。上述程序完全有效,适用于所有平台。我相信它有很大的潜力来创建类模式和许多其他东西。明智地使用它取决于公众。也许是使用 eval 的唯一正当理由之一。
  • 我想使用动态范围的原因是我可以将私有变量注入函数的范围。例如,在我上面的程序中,我还可以传递一个名为uber 的变量,它指向给定类的父类。这个变量应该可以被类范围访问,但它不应该被公众访问。因此,我不能只将它设置在类的实例上并称之为一天。因此,迂回的解决方法。
  • 下面的代码对你有用吗?变量 x=1;函数 g () { print(this.x);这个.x=2; } 函数 f () { 变量 x=3 ;这个.g(); } 打印(x);

标签: javascript eval dynamic-scope


【解决方案1】:

为什么没有人说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 值。

【讨论】:

  • 人们已经说过thisArmanThilo。如果您考虑一下,this 只是另一个论点。所以,你也不需要this。只需使用一个额外的参数。这正是ephemient 在他的回答中所描述的,这就是我接受它的原因。您的回答并没有真正增加任何价值。
  • 我猜你可以这样看。
【解决方案2】:

要添加有关此主题的注释:

无论何时在 JavaScript 中使用:

  • 函数声明语句或函数定义表达式则局部变量将具有词法作用域

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

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

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

【讨论】:

  • 很高兴知道this。谢谢。
【解决方案3】:

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

【讨论】:

  • 您的代码非常混乱。介意提供有意义的 cmets 来解释你在做什么吗?
  • @AaditMShah 已评论。然后去阅读Introduction to Object-Oriented JavaScript
  • 好吧,我终于明白你的代码了。您正在使用名为 dyn 的构造函数来模拟全局范围。每个函数都有一个名为env 的形式参数,它等效于该函数的激活对象。这个env 对象由调用者提供。对于函数f,我们提供dyn 的全局实例。对于g,我们提供全局dyn 的写时复制实例,将第一个实例推入原型链。这是一个直观的答案,范围查找确实类似于遍历原型链。加上一个用于寻找替代方案而不是诉诸使用eval。 =)
  • 也感谢您与我分享该链接 - 尽管我没有从中学到任何我不知道的东西,但这是一个很好的姿态。赞赏。
  • @AaditMShah 很高兴为您提供帮助。实际上,我的第一个想法(使用with 扩展范围)没有奏效……这一切都变得更好,因为它是 Javascript 的一个可怕特性:)
【解决方案4】:

我知道这并不能完全回答问题,但代码太多,无法放入评论中。

作为一种替代方法,您可能需要查看 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;
    }
});

【讨论】:

    【解决方案5】:

    在您的情况下,与其尝试使用动态范围设置构造函数,不如使用返回值?

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

    【讨论】:

    • 我可以这样做,但正如我在上一条评论中对我的问题所解释的那样 - 使用动态范围允许我将尽可能多的变量注入函数的范围。您只能返回一个值。
    【解决方案6】:

    如果您有办法做语法糖(例如带有 gensyms 的宏)并且您有 unwind-protect,您可以使用全局变量模拟动态范围。

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

    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,并且使用宏预处理器生成一些语法糖(这样您就不必手动打开所有保存和 unwind-protected 恢复的代码),它可能是可行的。

    【讨论】:

    • 您愿意用更简单的方式详细说明吗?也许将我链接到在线资源?
    • 我用 Lisp 的代码插图更新了评论;希望有帮助。
    • 我想我现在最好开始学习 Lisp。不过感谢您的帮助。赞赏。 =)
    • 看起来 JavaScript 的展开保护是 try { ... } finally { ... }。因此,您将在try 块中设置新值,然后将变量恢复为其在finally 中保存的值。 JavaScript 没有宏,但可以使用某种文本到文本的预处理器。 JavaScript 编码器无论如何都会使用这样的东西,比如压缩 JavaScript 代码的闭包“编译器”。
    • 这不是一个真正的选择。宏预处理器在编译时工作。我需要的是一个运行时解决方案。
    【解决方案7】:

    我不这么认为。

    这不是语言的工作方式。您必须使用变量以外的东西来引用此状态信息。我猜最“自然”的方式是使用this 的属性。

    【讨论】:

      猜你喜欢
      • 2018-07-18
      • 2022-01-12
      • 2013-06-02
      • 1970-01-01
      • 2021-07-10
      • 2014-06-09
      • 1970-01-01
      • 1970-01-01
      • 2011-12-15
      相关资源
      最近更新 更多