从任意评估代码中获取返回值



我有一个要求,即用户可以提供任意语句,这些语句可以存储在函数中,稍后调用以获取返回值。一个简单的例子是,userInput可能是

var x = 10;
x;

我会通过以下方式存储它

var callback = function() {
return eval(userInput);
}

然后运行callback()按预期返回10

但是,我还需要用明确的返回语句来支持这种情况,即userInput可能是

var x = 10;
return x;

在这种情况下,上面的 eval 方法将失败,并显示SyntaxError: return not in function。相反,我可以将回调存储为

var callback = new Function(userInput);

我的问题是我想根据规则"获取显式返回值,否则获取上次执行的语句的结果"将这两种方法结合起来。特别是,这不能通过在回调创建时分析代码来完成,因为用户可能会做一些奇怪的事情,例如

if(envVar < 10)
return a;
b * 0.5;

它结合了两者。

关于如何构建callback函数的创建以适应这些可能的输入的任何想法?不幸的是,不可能对用户强制执行一种或另一种样式。


更新以回答下面的一些评论。

建议的一种解决方案是搜索返回令牌并在new Functioneval之间进行选择。这不适用于我的最后一个示例,请参阅 http://jsfiddle.net/ZGb6z/2/- out4 应该是"no",但最终未定义,因为没有返回最后一个执行的语句。

另一个建议是修改用户输入,以便在最后一行添加显式返回(如果未找到)。不幸的是,不可能知道哪个语句将最后执行。考虑

var x = 10;
switch(x) {
case 10:
100;
break;
default:
200;
break;
}

当调用它时,它应该返回100,但是需要一些认真的分析来确定将任意代码的返回放在哪里。

只需使用trycatch,操作输入对您来说将非常痛苦,并且trycatch此时无法真正使您的代码更加迟钝。

var failback = function () {
try {
return eval(userInput);
} catch (e) {
return Function(userInput);
}
};

我真正建议的是投资一个解析器,有点像Angular的方式。这种事情会阻止你的用户做任何他们想做的事情,引入攻击媒介,yadda,yadda,yadda。

管理您的期望或管理您的用户的期望。 如果需要在同一用户输入中混合使用显式和非显式return语句,则evalnew Function()不适合您的要求。遵循这些任一路线,您将继续发现问题。

仅仅搜索return这个词也是不够的......var returning = true;var a = 'return';/* return true */ true;都会引发误报。

管理你的期望:要做这样的事情,你需要一种形式的词法分析和解析器,此时你可以完全取消eval,并根据解析的输入执行你自己的安全函数。当无论如何都必须执行用户输入时,这是最好的方法,因为您可以确保不会执行您不希望允许的任何内容。如果你想覆盖这些边缘情况并允许奇怪的用户输入,那么你必须准备好增加应用程序的大小和开发时间。我已经构建了一些执行用户生成代码的应用程序,并且总是得出结论,这是正确的路线。

管理用户的期望:提供一个指南,告诉他们不要将显式返回与非显式返回混合,无论如何,这些都是奇怪的编码实践。最好明确告诉他们包含或省略 return 语句。要求您的用户关注他们并不可耻,特别是如果它允许您在其他地方改善他们的体验。

在那里,我以为我只会在代码高尔夫堆栈交换:)看到这样的问题

我的解决方案在这里:http://jsfiddle.net/hKq87/1

它实质上将"return"语句替换为前缀为特殊字符串的异常。如果我们看到该字符串,我们知道我们实际上是在返回一个值,并返回它而不是重新引发异常。

我选择抛出异常而不是用函数调用替换return语句的原因是,很难知道为return计算的 JS 代码真正在哪里结束。它可以拆分为多行,包含多个特殊字符,甚至可能在末尾没有可选的分号。因此,我将字符串连接到返回的任何值并throw它,因为throw关键字不需要将其参数括在括号中。

此外,抛出异常为我提供了一种方便的方法,可以立即终止代码块的执行,而无需停止其他 JS 执行。

下面是回调方法:

var callback = function(userInput) {
var returned = undefined;
userInput = userInput.replace(/(^|[(\)[]{};,s])return(s*[^s;])?/gi, function(m, b, e){ 
return b + " throw '__RETURNED_VALUE'" +
(e !== undefined ? "+" + e : "");
});
try {
returned = eval(userInput);
} catch (e) {
if (e.indexOf("__RETURNED_VALUE") == 0) {
returned = e.substring("__RETURNED_VALUE".length) || undefined; 
}
else {
throw e; 
}
}
return returned;
}

上面的正则表达式考虑了可能以字符串"return"结尾的变量,我们不想替换这些变量,因为它不是return语句。它还允许在大括号内使用返回语句,而不使用尾随分号或在开头/结尾处。

当前方法的一个问题是,您不能在return语句中使用(罕见的)逗号运算符,或者期望正确返回数值。jsfiddle 中的最后一个测试用例证明了这一点。这里的例子:

//original
return 5 * 2 + 3,  22;
//modified
throw '__RETURNED_VALUE='+ 5 * 2 + 3,  22;
//apply * operator
throw '__RETURNED_VALUE='+ 10 + 3,  22;
//apply + operators
throw '__RETURNED_VALUE=103',  22;
//apply , operator
throw 22;

这个问题可以通过完全消除前缀"__RETURNED_VALUE="并用"throw"替换"return"来避免。但是,这意味着提供的代码必须在不抛出异常的情况下运行,我认为这是一个比将返回语句制作为简单的(避免逗号运算符、非括号算术等)更难的约束。此外,如果用户创建了一个我们无法用当前代码处理的 return 语句,我们可以方便地为其抛出异常,以便它很容易引起我们的注意。

jsFiddle Demo

让我们假设你的用户可能比普通熊聪明一点。我们将要求他们专门提供一个初始值,然后提供一个带有该值表达式的回调。

这样做的主要好处是可以避免 eval,并且实际上有一个可重用的良好实现,而不是以后重构。

这种方式还提供了输入来自何处和检查来自何处的分离。尽管提供的示例仅显示整数输入,但实际上它可能是另一个完全不知道值的调用,除了它需要符合回调逻辑之外。

function expression(x,callback){
return callback(x);
}
out1.innerHTML = expression(8,function(x){
return x;
});
out2.innerHTML = expression(10,function(x){
return x;
});
out3.innerHTML = expression(10,function(x){
if(x === 10) return "yes"; "no";
});
out4.innerHTML = expression(8,function(x){
return x === 10 ? "yes" : "no";
});

最新更新