为什么我只能在嵌套闭包链的第一个实例中声明变量?

时间:2013-10-17 00:52:41

标签: javascript closures continuations

我正试图在Javascript中破解Read-Eval-Print-Loop。 (这是基于网络的“自学Javascript”平台。)我有一些主要有用的东西,但是我遇到了一个关于闭包的奇怪错误。

这是循环核心的简化版本。我这样写是因为我想使用闭包和延续来维护eval中创建的任何状态:

// listing 1

var repl = function(result) {
  return {
    result:
      result,
    then:
      function (expression, continuation) {
        return continuation(eval(expression));
      },
  };
}

它几乎可以工作。例如,它正确地评估了表达式var x = 1x++x的序列:

// listing 2

repl(eval('var x = 1')).then('x++', repl)
                       .then('x', repl)
                       .result

// evaluates to 2

因此,表达式可以访问和修改先前声明的局部变量,而不会污染全局范围,这很好。 但变量声明(即var ...)仅适用于链中的第一个表达式。例如,表达式序列var x = 1var y = 2,{ {1}}抛出 y未定义错误:

y

我发现如果我用它的定义替换// listing 3 repl(eval('var x = 1')).then('var y = 2', repl) .then('y', repl) .result; // throws "y is not defined" 的每个实例,我就可以避免这个错误,如下所示:

repl

这评估为2.所以我想我可以通过//listing 4 // // same as listing 3, but repl is replaced with its // definition from listing 1 function (result) { return { result: result, then: function (expression, continuation) { return continuation(eval(expression)); } }, })(eval('var x = 1')).then( 'var y = 2', function (result) { return { result: result, then: function (expression, continuation) { return continuation(eval(expression)); }, }; } ).then( 'y', function (result) { return { result: result, then: function (expression, continuation) { return continuation(eval(expression)); }, }; } ).result // evaluates to 2 每次迭代的eval定义来解决我的问题。但肯定有更好的解决方案......不存在吗?


修改:我尝试用repl替换repl的每个实例,但它没有解决问题。我错过了什么?

2 个答案:

答案 0 :(得分:2)

eval 创建的变量是在调用执行上下文中创建的。因此,它们仅可用于稍后在同一范围链上创建的其他执行上下文。

如果你这样做:

eval( 'var x = 3' );

作为全局代码,然后在计算表达式时,将全局上下文中的值为 3 的变量 x 创建为变量。如果从函数上下文中调用 eval ,那么任何变量声明都是该上下文的本地声明:

function doEval()
  eval( 'var x = 3' );
}

当调用doEval()时, x 将在函数的执行上下文中创建,并且只能在函数内部使用。

E.g:

function doEval() {

  // Create local variable x
  eval( 'var x = 3' );

  // x is available on this function's scope chain
  var foo = function(){alert(x)};

  foo();
}

doEval()   // shows 3

alert(x) // Refernce error, x is not defined

在您的代码中,每个函数调用都会创建一个新的执行上下文和变量环境,该环境无法访问前一个。您无法获取对本地上下文或变量对象的引用,因此您无法将其传递给其他函数。

要评估多个字符串作为代码,您可以使用对 eval 的顺序调用,如:

function evaluateExpressions() {
  for (var i=0, iLen=arguments.length; i<iLen; i++) {
    eval(arguments[i]);
  }
}

并调用函数:

evaluateExpressions(expr1, expr2, expr3, ...)

然后你可能会这样做:

eval(expr1 + expr2 + expr3 + ...)

eval([expr1, expr1, expr3, ...].join(';'))

至少从函数内部调用它意味着变量不会意外地变为全局。

哦,差点忘了, eval是邪恶的。 ; - )

答案 1 :(得分:2)

由于RobG已经解释了问题的原因,我将这个答案限制在一个可能的解决方法。

如果您不介意污染全局范围(可能在这里甚至需要?),您可以使用indirect eval call强制全局评估所有表达式:

var repl = function(result) {
  return {
    result:
      result,
    then:
      function (expression, continuation) {
        return continuation((1,eval)(expression));
      },
  };
}

http://jsfiddle.net/y37Pj/

您也可以拨打repl而不是继续,因此您不需要每次都将其传递给then

var repl = function(result) {
  return {
    result:
      result,
    then:
      function (expression) {
        return repl((1,eval)(expression));
      },
  };
}

repl(eval('var x = 1')).then('var y = 2')
                       .then('y')
                       .result

http://jsfiddle.net/y37Pj/1/


Pitarou接受的解决方案隐藏在jsFiddle中,对此答案发表评论。作为参考,这里是“丑陋的黑客”解决方案的略微修改版本(它破坏了创建闭包的函数的源代码):

function make_repl() {
  return {
    result: undefined,
    then: function (expression) {
      return {
        result: eval(expression),
        then: eval('('+this.then.toString()+')'),
      };
    },
  };
};

var repl = make_repl();

repl = repl.then('var x = 1').then('var y = 2').then('x + " " + y');

console.log(repl.result); // prints "1 2"

console.log(typeof(x), typeof(y));
// prints "undefined undefined", so we know the global scope was not touched