能否在JavaScript中实现动态作用域而不使用eval?

28

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是词法作用域语言,因此它将按以下方式打印:

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

我知道这不是一个很好的例子,但总体想法是利用动态作用域创建闭包。我认为这种模式有很大的潜力。


1
写得很好,问题很有趣。虽然它要求客观答案,但除非有人展示如何做到,否则我怀疑它会引发很多争论和主观回答,解释为什么不能或不应该这样做。是什么引起了你对这个问题的兴趣? - Chris Wesseling
以下代码对你是否有效?var x=1; function g () { print(this.x); this.x=2; } function f () { var x=3 ; this.g(); } print(x); - Ken Russell
@RajkumarMasaniayan - 不好意思,你的代码非常复杂。函数g无法访问f的局部变量,除非它在f中定义。这就是我使用var g = eval(String(g))的原因。在你的情况下,它就像我上面的第一个例子(没有动态作用域的那个)一样糟糕。更糟糕的是,你正在通过使用this显式设置全局变量。 - Aadit M Shah
@Aadit - 以下代码可以实现你想要的功能,但我不确定它是否足够通用并且能处理所有边缘情况。我在这里进行说明,以防万一。var test = function() { this.x=1; var g = function g () { console.log(this.x); this.x=2; } var f = function f () { this.x=3; g.call(this); } this.f = f; new f(); console.log(this.x);} new test(); - Ken Russell
@RajkumarMasaniayan - 不,它不能做我想要的事情。我想要的是能够修改私有变量,而不是this属性。我相信使用eval是唯一的方法。然而,像每个程序员一样,我已经学会了拥抱语言而不是与之斗争。所以我不再需要动态作用域。我在函数式编程中找到了一个更好的解决方案,并利用我的新知识创建了一个非常小而高效的JavaScript库,用于面向对象和函数式编程-augment。=) - Aadit M Shah
显示剩余3条评论
7个回答

12

添加关于此主题的注释:

在JavaScript中,每当您使用以下内容时:

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

  • 函数构造函数,那么局部变量将引用全局作用域(顶级代码)。

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

所以,回答您的问题,在JS中,this已经是语言的动态作用域特性,您甚至不需要模拟另一个。


3
我很感激知道了这个。谢谢。 - Aadit M Shah

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

你的代码非常混乱。能否提供有意义的注释来解释你在做什么? - Aadit M Shah
@AaditMShah 评论了。请阅读一下JavaScript面向对象介绍 - ephemient
1
谢谢你也与我分享了那个链接,虽然我没有从中学到任何新的东西,但这是一个很好的举动,我很感激。 - Aadit M Shah
1
@AaditMShah 很高兴能帮忙。实际上,我的第一个想法(使用 with 扩展范围)没有奏效...这其实是件好事,因为这是 JavaScript 中一个可怕的特性 :) - ephemient
1
这个答案几乎无法阅读。 - niryo
显示剩余2条评论

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


人们已经说过了:Arman, Thilo。如果你仔细想想,this只是另一个参数而已。所以,你其实也不需要this。只需使用额外的参数即可。这正是ephemient在他的答案中描述的原因,也是我接受它的原因。你的答案并没有真正增加任何价值。 - Aadit M Shah
我猜你可以这样看。 - user68880

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

我可以这样做,但正如我在我的问题上最后一条评论中解释的那样 - 使用动态作用域允许我将尽可能多的变量注入到函数的作用域中。你只能返回一个值。 - Aadit M Shah

2

我不这么认为。

语言不是这样工作的。您必须使用其他内容来引用此状态信息,最自然的方式是使用this的属性。


0

如果您有一种进行语法糖(例如带有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恢复),那么这可能是可行的。


请您能否用简单的语言详细解释一下?或者给我提供一个在线资源的链接? - Aadit M Shah
我使用Lisp更新了注释,附上了代码示例;希望有所帮助。 - Kaz
我想我最好现在开始学习Lisp。不过还是谢谢你的帮助,非常感激。=) - Aadit M Shah
1
看起来 JavaScript 的取消保护是 try {...} finally {...}。因此,您将在 try 块中设置新值,然后在 finally 中将变量恢复为其保存的值。JavaScript 没有宏,但某种文本到文本预处理器可以使用。JavaScript 程序员无论如何都会使用这些东西,例如 Closure "编译器" 可以压缩 JavaScript 代码。 - Kaz
1
这并不是一个可选项。宏预处理器在编译时工作。我需要的是运行时解决方案。 - Aadit M Shah

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接