3

我正在尝试用 Javascript 编写一个 Read-Eval-Print-Loop。(它适用于基于 Web 的“自学 Javascript”平台。)我有一些最有效的东西,但我遇到了一个奇怪的闭包错误。

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

// listing 1

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

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

// listing 2

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

// evaluates to 2

所以表达式可以访问和修改之前声明的局部变量,而不会污染全局范围,这很棒。 但是变量声明(即var ...)仅适用于链中的第一个表达式。 例如,表达式序列var x = 1, var y = 2,y抛出一个y is not defined错误:

// listing 3

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

// throws "y is not defined"

我发现如果我repl用它的定义替换每个实例,我可以避免这个错误,如下所示:

//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

这评估为 2。所以我想我可以通过eval定义repl每次迭代的定义来解决我的问题。但肯定有更好的解决方案……不是吗?


编辑:我尝试用 替换每个实例repleval('('+repl+')')但它没有解决问题。我错过了什么?

4

2 回答 2

2

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

如果你这样做:

eval( 'var x = 3' );

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

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

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

例如:

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 是邪恶的。;-)

于 2013-10-17T01:37:24.120 回答
2

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

如果您不介意污染全局范围(也许这里甚至需要这样做?),您可以使用间接eval调用来强制对所有表达式进行全局评估:

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 中。作为参考,这里是“ugly hack”解决方案的略微修改版本(它评估函数的源代码以创建闭包):

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
于 2013-10-17T01:58:10.087 回答