我有一个要求,即用户可以提供任意语句,这些语句可以存储在函数中,稍后调用以获取返回值。一个简单的例子是,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 Function
和eval
之间进行选择。这不适用于我的最后一个示例,请参阅 http://jsfiddle.net/ZGb6z/2/- out4 应该是"no"
,但最终未定义,因为没有返回最后一个执行的语句。
另一个建议是修改用户输入,以便在最后一行添加显式返回(如果未找到)。不幸的是,不可能知道哪个语句将最后执行。考虑
var x = 10;
switch(x) {
case 10:
100;
break;
default:
200;
break;
}
当调用它时,它应该返回100
,但是需要一些认真的分析来确定将任意代码的返回放在哪里。
只需使用try
catch
,操作输入对您来说将非常痛苦,并且try
catch
此时无法真正使您的代码更加迟钝。
var failback = function () {
try {
return eval(userInput);
} catch (e) {
return Function(userInput);
}
};
我真正建议的是投资一个解析器,有点像Angular的方式。这种事情会阻止你的用户做任何他们想做的事情,引入攻击媒介,yadda,yadda,yadda。
管理您的期望或管理您的用户的期望。 如果需要在同一用户输入中混合使用显式和非显式return
语句,则eval
和new 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";
});