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

Javascript 为什么我只能在嵌套闭包链的第一个实例中声明变量?,javascript,closures,continuations,Javascript,Closures,Continuations,我正试图用Javascript编写一个Read-Eval-Print循环。(这是一个基于网络的“自学Javascript”平台。)我有一些基本有效的东西,但我遇到了一个关于闭包的奇怪错误 这里是循环核心的简化版本。我这样写是因为我想使用闭包和continuations来维护在eval中创建的任何状态: // listing 1 var repl = function(result) { return { result: result, then: f

我正试图用Javascript编写一个Read-Eval-Print循环。(这是一个基于网络的“自学Javascript”平台。)我有一些基本有效的东西,但我遇到了一个关于闭包的奇怪错误

这里是循环核心的简化版本。我这样写是因为我想使用闭包和continuations来维护在
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未定义错误:

我发现,如果我用它的定义替换
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
来解决我的问题。但肯定有更好的解决办法。。。不是吗


Edit:我尝试将
repl
的每个实例替换为
eval(“(“+repl+”)”)
,但没有解决问题。我缺少什么?

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

如果您这样做:

eval( 'var x = 3' );
作为全局代码,然后在计算表达式时,将值为3的变量x创建为全局上下文中的变量。如果从函数上下文调用eval,则任何变量声明都是该上下文的局部声明:

function doEval()
  eval( 'var x = 3' );
}
调用
doEval()
时,x将在函数的执行上下文中创建,并且仅在函数内部可用

例如:

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

要将多个字符串作为代码进行求值,可以使用顺序调用进行求值,如:

但你也可以这样做:

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

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


哦,差点忘了,评估是邪恶的;-)

既然RobG已经解释了问题的原因,我将把这个答案限制在一个可能的解决方法上

如果您不介意污染全局范围(这里甚至可能需要这样做?),可以使用强制全局计算所有表达式:

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

您也可以调用
repl
,而不是continuation,因此您不需要每次都将其传递到
然后

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


Pitarou接受的解决方案隐藏在对该答案的评论中的JSFIDLE中。作为参考,这里是“丑恶黑客”解决方案的一个稍加修改的版本(它评估函数的源代码以创建闭包):


第一次
eval
没有发生在您的REPL中。它发生在调用方的范围内,因为您正在那里调用
eval
。谢谢,Barmar。这是否解释了清单3和清单4之间的行为差异?嗯,我仍然完全困惑。为什么后续的eval可以看到调用者的作用域,并且可以操作其中的变量,但不能添加到该作用域中?我相信
eval()
在它所在的函数的作用域中运行。因此,当
repl()
调用
eval
时,它会修改自己的范围,但不会修改其他调用
eval
的范围。第一个是有效的,因为
eval
的所有调用都共享正在修改的全局范围。因此,函数的每次调用都有自己的作用域,即使它递归地调用自己。采用这种复杂方法的全部目的是,我希望REPL打印一个结果,等待用户输入下一行,然后在与前几行相同的上下文中执行下一行代码。您可以重新执行前几行表达式,将每个表达式保存在一个闭包中的数组中。在执行每个
之后
时,将当前表达式推送到数组上,然后
求值所有表达式。您需要一个清除表达式数组的
clear
reset
方法。是的,我使用了类似的方法来解决其他问题。REPL在WebWorker中运行,因此,如果它超时,系统将终止它,然后通过重放用于创建旧WebWorker的消息序列来初始化新WebWorker。当然,问题是,随着历史变得越来越长,它将变得缓慢而无反应。每次重播历史记录时,我还必须非常小心如何处理副作用。不过,这可能是可行的。非常感谢你的帮助。如果我不介意污染全球范围,整个练习将会非常简单!但是谢谢你给我看间接评估黑客。那个对我来说是新的!为什么我没想到呢?这是一个真正的进步。:-)@皮塔罗感谢您的建议编辑;我看到它太晚了,在中编辑了已接受的解决方案。我认为这是正确的,因为eval(“(“+this.then+”)与eval(expression)在同一个词法范围内执行,所以词法范围被保留下来了吗?@Pitarou是的,这会在每次
然后
调用中创建一个新的
然后
,以及新的f
evaluateExpressions(expr1, expr2, expr3, ...)
eval(expr1 + expr2 + expr3 + ...)
eval([expr1, expr1, expr3, ...].join(';'))
var repl = function(result) {
  return {
    result:
      result,
    then:
      function (expression, continuation) {
        return continuation((1,eval)(expression));
      },
  };
}
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
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